Skip to content

Commit eca5320

Browse files
Merge pull request #731 from corbitsdev/cl-7266-replace-advance_workflow-with-idempotent-step-tagged
Replace workflow advance with step-tagged submit_output
2 parents 1bd4978 + 1042db1 commit eca5320

20 files changed

Lines changed: 443 additions & 163 deletions

CHANGELOG.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,13 @@ parallel copies under `docs/` or `scripts/notes/`. At cut time: rename
1313

1414
## [Unreleased]
1515

16+
### Changed
17+
18+
- Completing a workflow step is a `submit_output` tagged with that step's id.
19+
`advance_workflow` is gone. Already-complete and not-current ids are
20+
acknowledged without advancing. The unused `autoAdvance` workflow field is
21+
removed.
22+
1623
### Fixed
1724

1825
- Failed sessions with an `error` string in `run.json` are valid resume

docs/ARCHITECTURE.md

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -173,8 +173,7 @@ Compaction replaces older turns with a structured, workflow-aware summary rather
173173

174174
- `ask_operator` — Pauses for a clarifying question with a list of options.
175175
- `present` — Renders structured UI from a JSON view spec instead of pasting tables into chat.
176-
- `submit_output` — Workflow step advancement when `step` is set (observed by the workflow coordinator).
177-
- `advance_workflow` — Advances the active workflow to its next step (observed by the director). Only advertised while a workflow is running.
176+
- `submit_output` — Completes a workflow step when `step` is set. The step id is compared atomically against the current step (`complete()`); already-complete ids (behind the cursor) and not-current ids (future or unknown) are acknowledged without advancing. Always advertised so activating a workflow does not grow the tools array.
178177

179178
Core agent tools (advertised in every chat turn) include `manage_tasks`, `tool_search`, `use_skill`, **`task`** (spawn a sub-agent), and **`search_agents`** when sub-agent profiles are available — see Sub-agents below.
180179

@@ -186,7 +185,7 @@ Workflows are named, ordered recipes the agent follows step by step — a thin l
186185
- `capabilities.ts``detectCapabilities` maps the live tool surface to abstract capabilities (`ticket-tracker`, `code-host`, `doc-search`) by name pattern; `resolveStep` decides whether a step runs. A capability override set forces integrations off per run. Adding a capability is a data edit, not a logic change.
187186
- `runtime.ts``WorkflowRuntime` drives execution on a call stack: it skips capability-unsatisfied steps, descends into sub-workflow references, emits step lifecycle events, and snapshots `WorkflowState`. `state.ts` persists that snapshot atomically to `workflow.json` under the session state root for resume.
188187

189-
- `coordinator.ts` — bridges runtime and director: produces the `[WORKFLOW STEP i/total: label]` directive injected into each turn's system prompt, and advances the runtime when `advance_workflow` (or a `submit_output` tagged `{ step }`) completes. Shared by both directors.
188+
- `coordinator.ts` — bridges runtime and director: produces the `[WORKFLOW STEP i/total: label]` directive injected into each turn's system prompt, and compare-and-advances the runtime when a `submit_output` tagged `{ step }` completes. Already-complete and not-current ids are acknowledged without moving the cursor. Shared by both directors. Fresh and resumed runs share one listener path.
190189
- The built-in recipes: the atomics `update-ticket`, `improve-docs`, `write-tests`, `triage-bug`, `code-review`, `scope-project`, and the `build-feature` composite that chains them.
191190

192191
Invocation: workflows are **not** top-level slash commands. Recipe definitions load into the `WORKFLOWS` registry from **enabled workflow/command plugins** at startup; command surfaces on those plugins (e.g. a workflow plugin's command prefix such as `/mywf scope`). Slash commands may also be authored as data-only markdown (`commands/*.md`, no `index.ts`); see PLUGINS.md. The model never suggests or auto-starts workflows from ordinary chat. Skills (bundled `corbits-skills`, enabled plugins, or `.agents/skills/`) load on demand via `use_skill` or as `/<skill-name>` slash commands when `user-invocable` is not `false` (see Skills below). The TUI surfaces state via `src/tui/workflow-controller.ts` (lifecycle, capability overrides, resume) — the header shows step progress (`⟳ name · step/total label`).

docs/IMPLEMENTATION.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -342,7 +342,7 @@ Positional arguments after flags are joined into the optional initial task deliv
342342
### Inference
343343

344344
- OpenAI-compatible chat completions, streamed via `@intx/inference`
345-
- JSON-schema tool definitions for director-layer tools (`ask_operator`, `present`, `submit_output`, `advance_workflow`) and agent tools (`manage_tasks`, `tool_search`, `use_skill`, `search_agents`, …)
345+
- JSON-schema tool definitions for director-layer tools (`ask_operator`, `present`, `submit_output`) and agent tools (`manage_tasks`, `tool_search`, `use_skill`, `search_agents`, …)
346346

347347
### State Persistence
348348

src/agent/director.ts

Lines changed: 25 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -99,9 +99,9 @@ const IDLE_OPEN_TASK_NUDGE =
9999

100100
const WORKFLOW_OPEN_TASK_NUDGE =
101101
"\n\nYou are ending your turn while tasks are still open (todo/doing) and a " +
102-
"workflow step is active. Continue working with tools, call advance_workflow " +
103-
"once the step is complete, or mark finished tasks done with manage_tasks. " +
104-
"Do not end your turn with tasks still open.";
102+
"workflow step is active. Continue working with tools, call submit_output " +
103+
"with this step's id once the step is complete, or mark finished tasks done " +
104+
"with manage_tasks. Do not end your turn with tasks still open.";
105105

106106
const DECLINED_OPEN_TASK_NUDGE =
107107
"\n\nThe operator declined the tool call. Do not retry the declined action. " +
@@ -243,27 +243,12 @@ export const presentDefinition: ToolDefinition = {
243243
},
244244
};
245245

246-
export const advanceWorkflowDefinition: ToolDefinition = {
247-
name: "advance_workflow",
248-
description:
249-
"Call this when the current workflow step is finished to advance to the next step. " +
250-
"Include an optional note summarizing what the step accomplished.",
251-
inputSchema: {
252-
type: "object",
253-
properties: {
254-
note: {
255-
type: "string",
256-
description: "Optional summary of what this step accomplished",
257-
},
258-
},
259-
},
260-
};
261-
262246
export const submitOutputDefinition: ToolDefinition = {
263247
name: "submit_output",
264248
description:
265-
"Call this when the task is fully complete (include summary) or to advance " +
266-
"a workflow step (include step id).",
249+
"Call this when the task is fully complete (include summary) or to complete " +
250+
"a workflow step (step id is required to advance; already-complete and " +
251+
"not-current step ids are acknowledged without advancing).",
267252
inputSchema: {
268253
type: "object",
269254
properties: {
@@ -274,8 +259,8 @@ export const submitOutputDefinition: ToolDefinition = {
274259
step: {
275260
type: "string",
276261
description:
277-
"Workflow step ID to advance. When present this is a " +
278-
"step-advancement signal, not a terminal task submission.",
262+
"Workflow step ID to complete. Required to advance a workflow. " +
263+
"Compared atomically against the current step.",
279264
},
280265
},
281266
},
@@ -480,12 +465,13 @@ class ChatDirectorImpl extends DefaultDirector {
480465
result: ReactorAction | ReactorAction[],
481466
): ReactorAction | ReactorAction[] {
482467
const active = this.workflowCoordinator?.isActive() === true;
483-
// advance_workflow rides on the wire every turn, workflow or not, so
468+
// submit_output rides on the wire every turn, workflow or not, so
484469
// activating a workflow never grows the tools array and busts the cache
485-
// prefix. Outside a workflow it is a harmless no-op the director ignores.
486-
const tools = this._toolDefinitions.some((t) => t.name === advanceWorkflowDefinition.name)
470+
// prefix. Outside a workflow it is a harmless no-op the director ignores
471+
// unless the call is a terminal task submission.
472+
const tools = this._toolDefinitions.some((t) => t.name === submitOutputDefinition.name)
487473
? this._toolDefinitions
488-
: [...this._toolDefinitions, advanceWorkflowDefinition];
474+
: [...this._toolDefinitions, submitOutputDefinition];
489475

490476
const directive = active ? (this.workflowCoordinator?.directive() ?? null) : null;
491477

@@ -694,7 +680,7 @@ class ChatDirectorImpl extends DefaultDirector {
694680
const path = pathResult instanceof type.errors ? "" : pathResult.path;
695681
if (isCodeFile(path)) this.lspTriggerCalls.add(block.id);
696682
}
697-
if (block.name === "advance_workflow" || block.name === "submit_output") {
683+
if (block.name === "submit_output") {
698684
this.workflowCalls.set(block.id, { name: block.name, args: block.arguments });
699685
}
700686
if (block.name === "ask_operator") {
@@ -791,10 +777,15 @@ class ChatDirectorImpl extends DefaultDirector {
791777
),
792778
];
793779
}
780+
const stepId = coordinator.currentStepId();
781+
const stepClause =
782+
stepId !== null
783+
? `call submit_output with { "step": "${stepId}" } now`
784+
: "call submit_output with this step's id now";
794785
const nudge =
795-
"\n\nYou have not yet called advance_workflow. " +
796-
"If this step is complete, call advance_workflow now. " +
797-
"Otherwise continue working with tools.";
786+
`\n\nYou have not yet completed this workflow step. ` +
787+
`If this step is complete, ${stepClause}. ` +
788+
`Otherwise continue working with tools.`;
798789
const passThrough = actions.filter(
799790
(a): a is Exclude<ReactorAction, { type: "wait" } | { type: "reply" }> =>
800791
a.type !== "wait" && a.type !== "reply",
@@ -816,8 +807,9 @@ class ChatDirectorImpl extends DefaultDirector {
816807
(a): a is Exclude<ReactorAction, { type: "wait" } | { type: "reply" }> =>
817808
a.type !== "wait" && a.type !== "reply",
818809
);
819-
// Inside a workflow the terminal action is advance_workflow, so point
820-
// the nudge at it rather than the general manage_tasks guidance.
810+
// Inside a workflow the terminal action is submit_output with the
811+
// current step id, so point the nudge at it rather than the general
812+
// manage_tasks guidance.
821813
const nudge =
822814
coordinator?.isActive() === true ? WORKFLOW_OPEN_TASK_NUDGE : IDLE_OPEN_TASK_NUDGE;
823815
return [...passThrough, inferWithNudge(capabilities, nudge)];

src/agent/prompts.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -219,7 +219,7 @@ const TOOL_SUMMARIES: Record<string, string> = {
219219
search_agents:
220220
"find agent profiles by role or team before spawning with task(agent=...); results include full system prompt / body so you need not read_file plugin roots outside the workspace",
221221
manage_tasks: "maintain your work checklist — create/replace, update status, append, cancel",
222-
submit_output: "signal the task is complete — the only way to finish",
222+
submit_output: "signal the task is complete, or complete a workflow step by passing its step id",
223223
ask_operator:
224224
"pause and ask the user when blocked or genuinely ambiguous; put long rationale in a transcript reply first, then call with a short question and short option labels only",
225225
present:

src/agent/tools.ts

Lines changed: 36 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,9 @@ import type { ToolDefinition } from "@intx/types/runtime";
44
import { type } from "arktype";
55
import { createPosixTools, type ToolPlugin } from "@intx/tools-posix";
66
import {
7-
advanceWorkflowDefinition,
87
askOperatorDefinition,
98
presentDefinition,
9+
submitOutputDefinition,
1010
} from "../agent/director.js";
1111
import { manageTasksDefinition } from "./tasks.js";
1212
import { validateView } from "../tui/view/index.js";
@@ -34,6 +34,7 @@ import { sessionModeEnablesSubAgents } from "../config/session-mode.js";
3434
import { advertisedToolNamesForSessionMode, type ToolAvailability } from "./tool-search.js";
3535
import type { ProviderCatalogEntry } from "../config/index.js";
3636
import type { AgentProfile } from "./profiles.js";
37+
import type { WorkflowCompleteResult } from "../workflows/types.js";
3738
import {
3839
createTaskTool,
3940
runSubAgent,
@@ -74,8 +75,9 @@ const AskOperatorArgs = type({
7475
options: "string[]",
7576
});
7677

77-
const AdvanceWorkflowArgs = type({
78-
"note?": "string",
78+
const SubmitOutputArgs = type({
79+
"summary?": "string",
80+
"step?": "string",
7981
});
8082

8183
// The operator can pick one of the offered options, type a free-form answer, or
@@ -131,10 +133,14 @@ export interface AgentToolsetArgs {
131133
getContextDir?: () => string | undefined;
132134
// Per-project settings.env, merged into the run_shell tool's spawn environment.
133135
shellEnv?: Record<string, string>;
134-
// Whether a workflow is currently running. advance_workflow rides the wire
136+
// Whether a workflow is currently running. submit_output rides the wire
135137
// every turn (workflow or not), so the model can call it with nothing active;
136138
// this lets its handler report an honest no-op instead of a false advance.
137139
isWorkflowActive?: () => boolean;
140+
// Compare-and-advance the live workflow. The handler reports this result
141+
// instead of reconstructing the cursor; omitted (exec, tests) never claims
142+
// an advance.
143+
completeWorkflowStep?: (stepId: string) => WorkflowCompleteResult;
138144
// Primary session mode (always orchestrator; kept for call-site wiring).
139145
sessionMode?: SessionMode;
140146
// Session-start facts gating lsp advertisement. Omitted callers (tests,
@@ -459,21 +465,35 @@ export async function createAgentToolset(args: AgentToolsetArgs): Promise<AgentT
459465
},
460466
}),
461467
stringTool({
462-
definition: advanceWorkflowDefinition,
463-
// The director observes this call and advances the workflow runtime; the
464-
// handler only needs to acknowledge so the model gets a clean tool result.
465-
// Since the tool is always advertised, the model can call it with no
466-
// workflow active — report the honest no-op rather than a false advance.
468+
definition: submitOutputDefinition,
469+
// The director also observes this call on tool.done; complete() is
470+
// compare-and-advance so a second pass is a no-op. The handler reports
471+
// complete()'s result so parallel submit_output cannot both claim an
472+
// advance. Already-complete and not-current ids succeed without
473+
// claiming one.
467474
handler: async (rawArgs: Record<string, unknown>): Promise<string> => {
468-
if (args.isWorkflowActive?.() === false) {
469-
return "No active workflow — nothing to advance.";
475+
const parsed = SubmitOutputArgs(rawArgs);
476+
const step = parsed instanceof type.errors ? undefined : parsed.step;
477+
const summary = parsed instanceof type.errors ? undefined : parsed.summary;
478+
const workflowActive = args.isWorkflowActive?.() === true;
479+
if (workflowActive) {
480+
if (step === undefined || step.length === 0) {
481+
return "Error: workflow completion requires a step identifier.";
482+
}
483+
const result = args.completeWorkflowStep?.(step) ?? "not-current";
484+
if (result === "advanced") {
485+
const note = summary !== undefined && summary.length > 0 ? ` (${summary})` : "";
486+
return `Workflow step marked complete${note}. Advancing to the next step.`;
487+
}
488+
if (result === "already-complete") {
489+
return "This workflow step is already complete. No advance.";
490+
}
491+
return "This workflow step is not current. No advance.";
470492
}
471-
const parsed = AdvanceWorkflowArgs(rawArgs);
472-
if (parsed instanceof type.errors) {
473-
return "Acknowledged.";
493+
if (step !== undefined && step.length > 0) {
494+
return "No active workflow — nothing to advance.";
474495
}
475-
const note = parsed.note !== undefined ? ` (${parsed.note})` : "";
476-
return `Workflow step marked complete${note}. Advancing to the next step.`;
496+
return "Acknowledged.";
477497
},
478498
}),
479499
];

0 commit comments

Comments
 (0)