-
Notifications
You must be signed in to change notification settings - Fork 3
Continue agents after workflow transitions #402
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -17,7 +17,7 @@ | |
| from mcp.server.transport_security import TransportSecuritySettings | ||
|
|
||
| from panopticon.core.artifacts import decode_b64_artifact, decode_segment, mcp_uri | ||
| from panopticon.core.models import Actor, Status | ||
| from panopticon.core.models import Actor, Status, Task | ||
| from panopticon.taskservice.api import TaskOut | ||
| from panopticon.taskservice.service import TaskService | ||
|
|
||
|
|
@@ -32,6 +32,32 @@ def _task(task: object) -> dict[str, Any]: | |
| return TaskOut.model_validate(task).model_dump(mode="json") | ||
|
|
||
|
|
||
| async def _transition_result( | ||
| service: TaskService, task: Task, *, acting_task: str | None | ||
| ) -> dict[str, Any]: | ||
| """Serialize a transition and put the entered phase's briefing in the tool result. | ||
|
|
||
| A transition happens mid-model-turn, after the user-prompt hook emitted the old phase's | ||
| briefing. Returning the new one here closes that context gap for every MCP-capable agent CLI. | ||
| """ | ||
| result = _task(task) | ||
| try: | ||
| briefing = await service.briefing_for(task) | ||
| except Exception: # The persisted transition must not look like a failed tool call. | ||
| _log.exception("task %s transitioned but its briefing could not be rendered", task.id) | ||
| return result | ||
| if ( | ||
| task.turn is Actor.AGENT | ||
| and not service.is_terminal(task) | ||
| and (acting_task is None or acting_task == task.id) | ||
| ): | ||
| briefing += ( | ||
| "\n\nYou now hold the turn in this phase. Continue its work immediately in this " | ||
| "same turn; do not stop merely to wait for another user prompt." | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [PLAUSIBLE] Second prose source for the same guidance. The continue-after-transition contract now lives in two places that must agree: this adapter-composed directive and its static paraphrase baked into every rendered skill file (
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Fixed in f712fbc. The rendered skill now only tells the agent to follow the briefing returned by the tool. The conditional continuation guidance lives solely in , so there is one dynamic prose source and one predicate. |
||
| ) | ||
| return {**result, "briefing": briefing} | ||
|
|
||
|
|
||
| def build_mcp_server(service: TaskService, *, name: str = "panopticon") -> FastMCP: | ||
| """An MCP server exposing the task service's agent-facing operations + artifacts.""" | ||
| # Disable the SDK's DNS-rebinding (Host/Origin) guard: the agent reaches us across the | ||
|
|
@@ -56,14 +82,18 @@ async def set_url(task_id: str, url: str) -> dict[str, Any]: | |
| return _task(await service.set_url(task_id, url)) | ||
|
|
||
| @mcp.tool(description="Apply a named core operation (e.g. 'advance', 'drop').") | ||
| async def apply_operation(task_id: str, operation: str) -> dict[str, Any]: | ||
| async def apply_operation( | ||
| task_id: str, operation: str, acting_task: str | None = None | ||
| ) -> dict[str, Any]: | ||
| _log.debug("mcp apply_operation task=%s operation=%s", task_id, operation) | ||
| return _task(await service.apply_operation(task_id, operation)) | ||
| task = await service.apply_operation(task_id, operation) | ||
| return await _transition_result(service, task, acting_task=acting_task) | ||
|
|
||
| @mcp.tool(description="Move the task to any state directly (free move; bypasses the gate).") | ||
| async def set_state(task_id: str, state: str) -> dict[str, Any]: | ||
| async def set_state(task_id: str, state: str, acting_task: str | None = None) -> dict[str, Any]: | ||
| _log.debug("mcp set_state task=%s state=%s", task_id, state) | ||
| return _task(await service.set_state(task_id, state)) | ||
| task = await service.set_state(task_id, state) | ||
| return await _transition_result(service, task, acting_task=acting_task) | ||
|
|
||
| @mcp.tool( | ||
| description="Resolve one promised responsibility ('met', or 'failed' with a comment)." | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[PLAUSIBLE] The directive misaddresses cross-task callers. "You now hold the turn… continue its work immediately" speaks to the tool caller, but
apply_operation/set_stateaccept anytask_idwith no caller scoping (only the orchestration tools are gated to the acting orchestrator). An Orchestrator moving a child back withset_state(child_id, "ITERATING")(turn_on_enter=AGENT) gets told to do the child's coding work in its own container — which has neither the child's branch nor its workspace.Cheapest fix: phrase it about the task, not the caller ("the task's agent now holds the turn — if that's you, continue…"); fuller fix is caller scoping on the transition tools.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Add an optional
acting_taskto resolve this issue. This will be useful when access controls are added later anyway.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed in f712fbc. and now accept optional ; rendered operations pass their own task ID. The continue directive is emitted only when the acting task is omitted for backward compatibility or matches the transitioned task. Added cross-task coverage.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Implemented in f712fbc. Both transition MCP tools expose optional , and generated operations send the current task ID in that field. It now controls whether the caller receives same-task continuation guidance and leaves a seam for later access-control enforcement.