Compare commits

...

9 Commits

Author SHA1 Message Date
Zamil Majdy
0ce734d28d refactor(backend): remove useless logger.debug from execution.py
Revert the early-continue refactor and logger.debug — debug logs aren't
read in production. Keep the simple inline guard style from the original
PR #12548.
2026-03-25 16:25:10 +07:00
Zamil Majdy
1a84a10af0 fix(backend): resolve merge conflict and improve debug log in execution.py
Merged dev into fix/copilot-tool-results-guidance. Resolved conflict in
from_db() by keeping the expanded early-return form with debug logging.
Improved the debug message to log block_id instead of block_type (which
is always OUTPUT at that point).
2026-03-25 14:36:25 +07:00
Zamil Majdy
ea0b398a57 fix(backend): add logger.debug and test for nameless node exec skipping
Address PR review comments:
- Add logger.debug when skipping OUTPUT node executions without 'name'
  in input_data (e.g. OrchestratorBlock nodes)
- Add test verifying GraphExecution.from_db handles mixed named/unnamed
  node executions without KeyError
2026-03-25 14:15:19 +07:00
Zamil Majdy
a6b4cd2435 refactor(backend/copilot): deduplicate tool-results guidance to system prompt only
Remove redundant tool-results mentions from Read and bash_exec tool
descriptions — the system prompt storage supplement already covers this
clearly. Avoids wasting tokens on triple-repeated instructions.
2026-03-25 13:37:43 +07:00
Zamil Majdy
9f418807a6 style: inline 'name' guard into existing condition 2026-03-25 13:31:54 +07:00
Zamil Majdy
cbc4bee9d8 fix(backend): guard against missing 'name' in execution input_data
Prevents KeyError when node executions from non-standard blocks
(e.g., OrchestratorBlock) don't include 'name' in input_data.
The from_db fallback assumes all INPUT/OUTPUT block executions have
a 'name' field, but this isn't guaranteed for all block types.
2026-03-25 13:21:39 +07:00
Zamil Majdy
3273b9558d fix(backend/copilot): revert filesystem note — Read shares sandbox too
Read shares the sandbox filesystem for regular files; it just
additionally reads host-side tool-results. Removing it from the list
was misleading. The tool-results section below already covers the
distinction.
2026-03-25 13:06:18 +07:00
Zamil Majdy
d0f558dc68 fix(backend/copilot): clarify Read tool's dual access in filesystem note
Address self-review: the "Shell & filesystem" section listed Read
alongside bash_exec as sharing one filesystem, which is misleading now
that Read also accesses host-side SDK tool-result files.
2026-03-25 12:53:58 +07:00
Zamil Majdy
8bb828804b fix(backend/copilot): steer model away from bash_exec for SDK tool-result files
The model was using bash_exec (cd + cat) to read SDK tool-result files
under ~/.claude/projects/.../tool-results/, which fails in E2B because
the sandbox runs as /home/user and cannot access /root/. The Read tool
handles these correctly by reading from the host filesystem.

Changes:
- Add "CANNOT read SDK tool-result files" warning to bash_exec description
- Add tool-results mention to the Read tool description
- Fix system prompt: replace stale "read_file" reference with "Read",
  explicitly warn that bash_exec cannot access host-side tool-results
2026-03-25 12:51:32 +07:00

View File

@@ -0,0 +1,121 @@
"""Tests for GraphExecution.from_db — verify node executions without 'name'
in input_data (e.g. OrchestratorBlock) are skipped gracefully."""
from datetime import datetime, timezone
from unittest.mock import MagicMock, patch
from prisma.enums import AgentExecutionStatus
from backend.blocks._base import BlockType
from backend.data.execution import GraphExecution
def _make_node_execution(
*,
exec_id: str,
block_id: str,
input_data: dict | None = None,
output_data: list | None = None,
status: AgentExecutionStatus = AgentExecutionStatus.COMPLETED,
) -> MagicMock:
"""Create a minimal mock AgentNodeExecution for from_db."""
ne = MagicMock()
ne.id = exec_id
ne.agentNodeId = f"node-{exec_id}"
ne.agentGraphExecutionId = "graph-exec-1"
ne.executionStatus = status
ne.addedTime = datetime(2024, 1, 1, tzinfo=timezone.utc)
ne.queuedTime = datetime(2024, 1, 1, tzinfo=timezone.utc)
ne.startedTime = datetime(2024, 1, 1, tzinfo=timezone.utc)
ne.endedTime = datetime(2024, 1, 1, tzinfo=timezone.utc)
ne.stats = None
ne.executionData = input_data or {}
ne.Input = []
ne.Output = output_data or []
node = MagicMock()
node.agentBlockId = block_id
ne.Node = node
ne.GraphExecution = None
return ne
def _make_graph_execution(node_executions: list) -> MagicMock:
"""Create a minimal mock AgentGraphExecution."""
ge = MagicMock()
ge.id = "graph-exec-1"
ge.userId = "user-1"
ge.agentGraphId = "graph-1"
ge.agentGraphVersion = 1
ge.inputs = None # Force fallback to node-based extraction
ge.credentialInputs = None
ge.nodesInputMasks = None
ge.agentPresetId = None
ge.executionStatus = AgentExecutionStatus.COMPLETED
ge.startedAt = datetime(2024, 1, 1, tzinfo=timezone.utc)
ge.endedAt = datetime(2024, 1, 1, tzinfo=timezone.utc)
ge.stats = None
ge.isShared = False
ge.shareToken = None
ge.NodeExecutions = node_executions
return ge
INPUT_BLOCK_ID = "input-block-id"
OUTPUT_BLOCK_ID = "output-block-id"
ORCHESTRATOR_BLOCK_ID = "orchestrator-block-id"
def _mock_get_block(block_id: str):
"""Return a mock block with the right block_type for each ID."""
block = MagicMock()
if block_id == INPUT_BLOCK_ID:
block.block_type = BlockType.INPUT
elif block_id == OUTPUT_BLOCK_ID:
block.block_type = BlockType.OUTPUT
else:
block.block_type = BlockType.STANDARD
return block
@patch("backend.data.execution.get_block", side_effect=_mock_get_block)
def test_from_db_skips_node_executions_without_name(mock_get_block: MagicMock):
"""Node executions without 'name' in input_data (e.g. OrchestratorBlock)
must not cause a KeyError and should be silently skipped."""
named_input = _make_node_execution(
exec_id="ne-input-1",
block_id=INPUT_BLOCK_ID,
input_data={"name": "query", "value": "hello"},
)
unnamed_input = _make_node_execution(
exec_id="ne-orchestrator-1",
block_id=INPUT_BLOCK_ID,
input_data={"value": "no name field here"},
)
named_output = _make_node_execution(
exec_id="ne-output-1",
block_id=OUTPUT_BLOCK_ID,
input_data={"name": "result", "value": "world"},
)
unnamed_output = _make_node_execution(
exec_id="ne-orchestrator-2",
block_id=OUTPUT_BLOCK_ID,
input_data={"value": "no name here either"},
)
standard_node = _make_node_execution(
exec_id="ne-standard-1",
block_id=ORCHESTRATOR_BLOCK_ID,
input_data={"some_key": "some_value"},
)
graph_exec_db = _make_graph_execution(
[named_input, unnamed_input, named_output, unnamed_output, standard_node]
)
result = GraphExecution.from_db(graph_exec_db)
# Named input extracted correctly
assert result.inputs == {"query": "hello"}
# Named output extracted; unnamed output skipped
assert dict(result.outputs) == {"result": ["world"]}