You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
AWF acts as a transparent ACP (Agent Client Protocol) endpoint over stdio, allowing ACP-compatible editors (Zed, acp.nvim, future VS Code/JetBrains plugins) to spawn awf acp-serve as the agent subprocess
New pkg/acpserver/ public package: stdlib-only, bidirectional JSON-RPC 2.0 engine with stdio serve loop, 10 MiB scanner ceiling, error code constants, and AST-enforced architecture invariant (mirrors pkg/mcpserver/). Bidirectionality serves only the agent→client session/request_permission callback in v1 — no fs/+terminal/ calls are issued (per decision #2(b))
Hidden awf acp-serve subcommand with skipFormatValidation annotation
Workflow execution as the unit of an ACP turn: session/prompt triggers a full ExecutionService run, not single-provider passthrough
Workflow discovery via ACP slash commands (available_commands_update); --input= values carried as slash-command arguments
Mid-run user input via ACPInputReader implementing ports.UserInputReader (channel bridge mirroring tui.TUIInputReader), with turn-boundary resume semantics (one conversation turn = one session/prompt → session/update* → stopReason cycle)
Approval gates routed through native session/request_permission
DisplayEvent → ACP session/update projection (agent_message_chunk, agent_thought_chunk, tool_call, tool_call_update) via a new ACP RenderFunc sink plugged into StreamFilterWriter
ContentBlock flattening (text, resource_link) for prompt ingestion
MCP passthrough: session/new.mcpServers forwarded into the existing F099 MCP proxy
Per-provider parser coverage for stable tool_call ID correlation
Hexagonal placement: domain port ports.ACPClient, application ACPSessionService, infrastructure adapter, .go-arch-lint.yml updated with pkg-acpserver / infra-acp components
Out of Scope
HTTP/WebSocket remote transport (stdio only in v1)
Honoring editor fs/ and terminal/ client methods (AWF keeps its own builtins + MCP proxy as execution substrate; tool calls are reported only)
CLI-launched awf run with IDE-attach relay (non-standard pattern, deferred)
session/load / session resume
Roles-as-modes / configurable ACP session modes
Workflow-as-single-agent passthrough mode (full workflow execution is the only unit)
session/set_mode, authenticate flow (no auth methods advertised in v1)
Deferred
Item
Rationale
Follow-up
Remote ACP transport over HTTP/SSE
Stdio is the only standardized transport AWF needs for v1 editors; remote transport in ACP spec is still evolving
future (could reuse ADR-016 Huma+SSE)
fs/+terminal/ client-capability use
Keeping AWF substrate (option b) preserves existing observability and avoids dual-execution paths
future flag
session/load resume
AWF already has session resume internally; ACP wiring can be added once stdio v1 stabilizes
future
AWF roles surfaced as ACP modes
Mode UX semantics in ACP not yet load-bearing for v1
future
Single-provider passthrough as alternative execution unit
Full-workflow execution is the canonical AWF surface
none (resolved)
Authentication methods
No agent CLIs require ACP-level auth; per-provider auth happens at AWF startup
future
User Stories
US1: Editor talks to AWF as a transparent ACP agent (P1 - Must Have)
As a developer using an ACP-compatible editor (Zed, acp.nvim), I want to connect my editor's agent panel to awf acp-serve and invoke any AWF workflow as a slash command, So that I get a uniform agent surface in my editor while AWF transparently routes prompts through whichever workflow/provider I configured, with observability, MCP tool interception, and history applied automatically.
Why this priority: This is the entire value proposition — without single-turn workflow execution exposed over ACP, the feature delivers nothing. It is the minimum viable surface that lets an editor drive AWF.
Acceptance Scenarios:
Given an editor configured to spawn awf acp-serve as an external agent, When the editor sends initialize followed by session/new, Then AWF responds with negotiated protocolVersion, advertised agentCapabilities, no authMethods, and an available_commands_update listing every workflow discovered in configs/workflows/ as a slash command.
Given an open ACP session, When the editor sends session/prompt invoking /my-workflow with text content blocks, Then AWF executes the full workflow via ExecutionService, projects workflow step structure (a tool_call / tool_call_update per workflow step plus, optionally, a plan for the step graph), streams agent_message_chunk notifications for assistant text and tool_call / tool_call_update for tool invocations within steps, and terminates the turn with PromptResponse.stopReason = end_turn — i.e. the editor sees multi-step progress, not a single flattened agent stream.
Given a workflow that requires --input=name=value parameters, When the editor sends /my-workflow --input=foo=bar, Then AWF parses slash-command arguments, validates required inputs, and surfaces missing-input errors as session/update text + a non-end_turn stop reason.
Independent Test: Spawn awf acp-serve via a stdio harness, send a recorded initialize + session/new + session/prompt JSON-RPC sequence, assert the notification stream contains at least one agent_message_chunk and the final response carries a valid stopReason. Verify command discovery by asserting the available_commands_update notification names every workflow file present in the fixture directory.
US2: Multi-turn conversation with mid-workflow user input (P2 - Should Have)
As a developer driving an AWF workflow that contains conversational agent steps, I want the workflow to pause for my input at conversation turn boundaries and resume when I send the next session/prompt, So that I can have a back-and-forth conversation with the underlying agent across multiple ACP turns without restarting the workflow.
Why this priority: AWF's conversational workflows (TUI mode) are a primary differentiator. Without ACP turn-boundary resume, editor users would be locked out of any workflow that uses ConversationManager.ExecuteConversation with multi-turn semantics. P2 because US1 is usable for non-conversational workflows on its own.
Acceptance Scenarios:
Given a workflow step that uses ConversationManager and calls UserInputReader.ReadInput, When the workflow goroutine blocks on ACPInputReader.ReadInput, Then AWF emits the buffered assistant turn, ends the current ACP turn with stopReason = end_turn, and parks the workflow goroutine on the response channel for the lifetime of the session.
Given a parked workflow goroutine, When the editor sends a follow-up session/prompt with text content, Then AWF routes the text to ACPInputReader.Respond(text), unparks the goroutine (does not start a new workflow), resumes execution, and emits the next assistant turn as a fresh session/update stream ending in a new stopReason.
Given a parked workflow goroutine, When the editor sends session/prompt with empty text, Then AWF returns StopReasonUserExit from the conversation loop, allows the workflow to terminate cleanly, and reports the final stopReason to the editor.
Independent Test: Drive a fixture workflow whose step blocks on user input. Send session/prompt #1, assert the response stops with end_turn and emits a marker agent_message_chunk. Send session/prompt #2 with different text, assert Respond was invoked (verifiable via instrumentation hook or a stub agent that echoes the input), and that no new workflow was started (workflow start counter unchanged).
US3: Cancellation interrupts a running workflow turn (P2 - Should Have)
As a developer running a long workflow from my editor, I wantsession/cancel to interrupt the active turn cleanly without leaking goroutines, subprocesses, or partial state, So that I can recover the editor's agent panel without killing AWF or my session.
Why this priority: Without cancellation, a long or stuck workflow makes the ACP session unusable. P2 because basic execution (US1) and conversation (US2) can ship first; cancellation hardens the surface.
Acceptance Scenarios:
Given an in-flight session/prompt running an ExecutionService workflow, When the editor sends session/cancel with the active sessionId, Then AWF cancels the execution context, propagates cancellation to all in-flight agent subprocesses (SIGTERM with 5s SIGKILL fallback), and concludes the current turn with stopReason = cancelled.
Given an in-flight tool_call reported via session/update, When cancellation completes, Then AWF emits a final tool_call_update marking the tool status as cancelled before the prompt response is returned.
Independent Test: Start a session/prompt invoking a workflow with a sleep step. Within 1 second, send session/cancel. Assert: response received within 6 seconds (5s SIGTERM grace + 1s), stopReason == cancelled, no orphan subprocesses, no leaked goroutines (verified via go test -race + leak detector).
US4: Tool-call approval gates surface as ACP permission requests (P2 - Should Have)
As a developer with workflows containing approval-gated tool calls, I want the editor to present the approval dialog natively via session/request_permission, So that I can allow/deny tool invocations from inside my editor without a separate TUI.
Why this priority: P2 because the routing mechanism is a locked decision (W3 → FR-009 is a MUST): binary approval gates route through native session/request_permission. The full deny-semantics UX (skip/error/abort) is incremental and builds on AWF's existing approval/interactive step machinery, but the core wiring must ship with the conversational surface.
Acceptance Scenarios:
Given a workflow step configured with an approval gate, When the step reaches the gate, Then AWF sends a session/request_permission Client method call with a structured prompt and options (allow, deny), pauses the step, and resumes based on the editor's response.
Given the user denies via the editor, When the response arrives, Then AWF skips the gated tool call and continues per the step's deny semantics (skip / error / abort), emitting a corresponding tool_call_update.
Independent Test: Run a fixture workflow whose first step is an approval gate. Drive session/request_permission callbacks via a stub ACP client that auto-replies allow then deny. Assert each outcome produces the expected tool_call_update status and that the workflow progresses or terminates as configured.
Edge Cases
What happens when the editor sends session/prompt with content blocks AWF does not support (image, audio, embedded resource)? → Reject with a session/update text chunk explaining the unsupported block, end the turn with a non-end_turn stop reason.
How does the system handle a malformed JSON-RPC frame, a line exceeding the 10 MiB scanner ceiling, or an unknown method? → Return the standard JSON-RPC error codes (-32700, -32600, -32601, -32602, -32603) without terminating the serve loop.
What is the behavior when a provider's parser does not emit Delta == true text fragments? → AWF degrades gracefully, emitting whole-message agent_message_chunk notifications rather than failing the turn.
What happens when a provider does not populate DisplayEvent.ID for a tool event? → AWF synthesizes a stable correlation ID per tool invocation so that tool_call_update notifications can be matched without provider changes.
What happens when session/prompt arrives while another prompt is in-flight on the same sessionId? → AWF rejects with an INVALID_REQUEST-class error; concurrent prompts on the same session are not supported in v1.
What happens when the workflow goroutine is parked on ACPInputReader.ReadInput and the editor closes the stdio connection? → AWF cancels the session context, unparks the goroutine with a cancellation signal, and lets the workflow tear down cleanly.
What happens when session/new.mcpServers is provided alongside a workflow whose steps already configure mcpProxyConfig? → Editor-provided MCP servers are merged into the per-step proxy config with editor entries taking precedence on key collision; document the merge rule in ADR-018.
Requirements
Functional Requirements
FR-001: System MUST expose a hidden awf acp-serve Cobra subcommand with the skipFormatValidation annotation so --format validation in PersistentPreRun does not exit the protocol-only command.
FR-002: System MUST implement a stdio JSON-RPC 2.0 engine in pkg/acpserver/ with stdlib-only imports, enforced by an AST-based architecture_test.go invariant identical in spirit to pkg/mcpserver/.
FR-003: System MUST implement initialize, session/new, session/prompt, and session/cancel ACP Agent methods, plus session/update notifications, conforming to a single pinned ACP protocolVersion integer documented in ADR-018.
FR-004: System MUST translate DisplayEvent values produced by every existing per-provider DisplayEventParser into ACP session/update variants per the mapping: EventText delta → agent_message_chunk; reasoning text → agent_thought_chunk; EventToolUse first sighting → tool_call; subsequent same-ID EventToolUse → tool_call_update.
FR-005: System MUST treat session/prompt as a full workflow execution entry point, invoking ExecutionService (the awf run machinery) rather than a single-provider passthrough.
FR-006: System MUST advertise every discoverable workflow as an ACP slash command via available_commands_update on session/new, with --input=key=value arguments mapped from slash-command arguments and validated against workflow input definitions.
FR-007: System MUST provide an ACPInputReader implementing ports.UserInputReader that bridges between the blocking ConversationManager goroutine and the ACP connection: ReadInput(ctx) blocks on a response channel; the next inbound session/prompt calls Respond(text) to unpark the goroutine; empty text returns StopReasonUserExit.
FR-008: System MUST implement turn-boundary resume semantics: when ReadInput blocks, AWF ends the current ACP turn with stopReason = end_turn and keeps the workflow goroutine parked across ACP turns for the lifetime of the session.
FR-009: System MUST route binary approval gates to native ACP session/request_permission Client-method calls without parking the workflow goroutine on the conversation input channel.
FR-010: System MUST honor session/cancel by cancelling the active execution context, propagating SIGTERM with a 5-second SIGKILL fallback to in-flight agent subprocesses, and terminating the current turn with stopReason = cancelled.
FR-011: System MUST forward session/new.mcpServers into the per-step mcpProxyConfig consumed by the existing F099 MCP proxy, with editor-provided entries taking precedence on key collision. (This merge-precedence rule was introduced by this spec — it resolves research open-question F004: Persistance d'État JSON #6, which was not user-decided — and MUST be ratified in ADR-018.)
FR-012: System MUST keep its own filesystem and terminal substrate (no editor fs/+terminal/ callbacks) in v1, advertising minimal or zero fs+terminal client-capability needs at initialize.
FR-013: System MUST return JSON-RPC error codes (-32700 parse error, -32600 invalid request, -32601 method not found, -32602 invalid params, -32603 internal error) for malformed input without terminating the serve loop, and MUST add USER.ACP.* validation codes mapped to exit code 1 for configuration errors surfaced at startup.
FR-014: System MUST flatten supported ACP ContentBlock variants (text, resource_link) into the prompt string passed to the workflow; unsupported variants (image, audio, embedded resource) MUST return a textual rejection notification and a non-end_turn stop reason.
FR-015: System MUST register pkg-acpserver and the infrastructure ACP adapter in .go-arch-lint.yml with dependency rules consistent with pkg-mcpserver / infra-mcp.
FR-016: System MUST project workflow-level execution events — step start/finish, parallel fan-out, and step errors — onto ACP session/update notifications, independently of the per-step DisplayEvent stream of FR-004. The workflow-level projector surfaces the multi-step structure of a workflow run to the editor (e.g. a tool_call / tool_call_update per workflow step, and/or a plan entry describing the step graph) so the editor sees workflow progress rather than a single flattened agent stream. FR-004 (inner agent-step stream) and FR-016 (outer workflow stream) compose: agent text/tool events emitted within a step are correlated under that step's projection. The projector MUST consume the workflow lifecycle / step events that ExecutionService already emits through the ports.EventPublisher port (workflow.started/completed/failed, step.started/completed/failed/retrying, including per-branch events for parallel steps) — no new ExecutionService instrumentation is required (verified 2026-05-30). Because ExecutionService is wired to a single EventPublisher (the plugin EventBus), the ACP projector MUST be attached via a fan-out EventPublisher (or an EventBus subscription) so it receives events in parallel with plugins without displacing them.
Non-Functional Requirements
NFR-001: pkg/acpserver/ MUST be stdlib-only with zero internal/ imports, enforced by architecture_test.go; the concurrency-heavy ACP serve loop and per-session goroutines MUST reach >85% unit test coverage and pass make test-race with zero data races. (ACP is JSON-RPC over stdio — there is no gRPC in this feature.)
NFR-002: Streaming latency from a back-end agent emitting a text fragment to AWF writing the corresponding agent_message_chunk to stdout MUST be under 100ms at p95 measured with a synthetic provider stub.
NFR-003: The ACP serve loop MUST use bufio.Scanner with a 10 MiB line ceiling (maxRequestLineBytes) so large payloads (base64 images, big diffs) do not truncate; oversize lines MUST produce a structured error rather than crash.
NFR-004: All goroutines spawned per session (reader, workflow, ACP renderer, parked input goroutines) MUST be cleaned up on session termination or stdio close, verified via leak-detector tests; subprocess termination MUST use a 5-second SIGTERM grace before SIGKILL.
NFR-005: Dry-run and preview workflow execution paths MUST remain infrastructure-free; ACP serving is a live path only and MUST NOT be wired into runDryRun or runInteractive.
NFR-006: Secrets masked elsewhere (vars starting with SECRET_, API_KEY, PASSWORD) MUST also be masked in ACP session/update text projections; ACP wire frames MUST NOT leak masked values.
Success Criteria
SC-001: An ACP-compatible editor (Zed external agent or acp.nvim) spawning awf acp-serve can list all configured workflows as slash commands within 1 second of session/new completion.
SC-002: Executing a workflow via /<workflow-name> from the editor produces the same final output and tool-call trace as awf run <workflow-name> on the CLI, measured against a fixed fixture matrix covering all six providers.
SC-003: A 5-turn conversational session (one ACP session, 5 session/prompt cycles) runs to completion with zero goroutine leaks and zero orphan subprocesses, verified by a race-and-leak integration test.
SC-004: session/cancel returns control to the editor within 6 seconds at p95 (5s SIGTERM grace + 1s overhead) regardless of which workflow step is active.
SC-005: 100% of existing per-provider DisplayEventParser outputs (covered by *_parse_display_events_test.go) map cleanly onto an ACP session/update variant via the new ACP renderer, asserted by table-driven mapping tests.
SC-006: make build, make lint, make test, and make test-race all pass with zero violations after F102 lands; the architecture_test.go invariant blocks any internal/ import added to pkg/acpserver/.
Key Entities
Entity
Description
Key Attributes
ACPSession
A live ACP session backed by a workflow execution and a parked conversation goroutine
Native ACP approval gate representation routed via session/request_permission
sessionId, toolCallID, prompt, options
ACPClient
Domain driven port for agent→client callbacks; in v1 scoped to session/request_permission only (no fs/+terminal/ per decision #2(b))
sessionId, RequestPermission(toolCall, options)
WorkflowEventProjector
Converts workflow-level execution events (step start/finish, parallel fan-out, errors) into session/update notifications; distinct from the per-step ACPRenderer (FR-016)
sessionId, conn, stepName, status, executionEvent
Assumptions
ACP protocol version is pinned to a single integer documented in ADR-018; the schema is consulted before coding to confirm the current major (likely 1).
Every existing provider DisplayEventParser populates DisplayEvent.ID consistently for tool events, or AWF synthesizes a stable per-invocation ID where the provider does not (verified via *_parse_display_events_test.go audit).
Editors driving AWF as an ACP agent support slash commands rendered from available_commands_update (Zed natively does; acp.nvim does).
Concurrent session/prompt calls on the same sessionId are not required in v1; sequential turn semantics match both the TUI input bridge and the ACP session/prompt lifecycle.
The existing TUIInputReader channel pattern (internal/interfaces/tui/input_reader.go) is structurally fit for ACP turn-boundary resume; no ConversationManager changes are required to support a long-parked goroutine across multiple ACP turns.
session/new.mcpServers can be merged into per-step mcpProxyConfig without breaking F099 MCP proxy invariants documented in ADR-017.
Verified 2026-05-30: ExecutionService already emits push events for every workflow/step transition via the ports.EventPublisher port (emitEvent() → Publish(*pluginmodel.DomainEvent)), including individually-named events for parallel branches. FR-016 is therefore buildable on existing hooks with no new ExecutionService instrumentation. The one wiring task is a fan-out EventPublisher: ExecutionService currently accepts a single publisher (the plugin EventBus), so the ACP workflow-event projector must be multiplexed alongside it rather than replacing it. (TUI and HTTP/SSE today poll GetAllStepStates() every 200ms instead of subscribing; the ACP projector should prefer the push path to meet low-latency goals.)
Q: Control model — IDE-driven (editor spawns awf acp-serve) or CLI-launched awf run with IDE-attach relay? → A: IDE-driven (native ACP). CLI-launched + IDE-attach relay is out of scope for v1.
Session 2026-05-30
Q: Execution unit per session/prompt — single-provider passthrough or full workflow execution? → A: Full workflow execution via ExecutionService / awf run machinery. Every step (agent, parallel, terminal), nested conversation, and approval moment projects onto ACP notifications.
Q: Transport — stdio only, or also HTTP/WS in v1? → A: stdio only for v1. HTTP/WS deferred (could later reuse ADR-016 Huma+SSE).
Q: fs/+terminal/ routing — honor editor client methods (a) or keep AWF substrate (b)? → A: (b) AWF substrate. AWF retains its builtins/MCP-proxy as execution substrate and only reports tool calls back as session/update. Option (a) deferred behind a future flag.
Q: Workflow selection mechanism — magic prefix in prompt, separate config, or ACP slash commands? → A: ACP slash commands via available_commands_update. Workflow --input= carried as slash-command arguments.
Q: Mid-run user input semantics — new ACP session per turn, single long-lived session with prompt-as-input, or other? → A: Single long-lived ACP session; the next inbound session/prompt is routed to Respond(text) on the existing ACPInputReader, mirroring TUIInputReader. Binary approval gates use native session/request_permission.
Q: Mid-run resume strategy — turn-boundary resume (A) or continuous within one turn (B)? → A: Turn-boundary resume (option A). ReadInput ends the current ACP turn with stopReason = end_turn and the workflow goroutine stays parked on responseCh for the lifetime of the session.
Notes
F102 mirrors F099/ADR-017 in the opposite direction: ADR-017 made AWF an MCP server to intercept agent → tool calls; F102 makes AWF an ACP agent to receive editor → prompts. Both are JSON-RPC 2.0 over stdio; both get a public pkg/ engine and a hidden serve subcommand.
ADR-018 to be authored alongside Phase 0, mirroring ADR-017's structure: protocol choice, topology, public-package rule, error taxonomy (USER.ACP.*), decision rationale.
Open implementation questions for Phase 1 planning: (1) confirm current ACP protocolVersion integer from the schema; (2) audit DisplayEvent.ID population across all provider parse tests; (3) ratify the session/new.mcpServers merge rule (editor precedence on collision, per FR-011) in ADR-018 and reconcile it with the existing mcpProxyConfig per-step wiring; (4) implement the fan-out EventPublisher and the workflow-level DomainEvent→session/update projector (FR-016) on top of the step/workflow events ExecutionService already emits — no ExecutionService instrumentation needed (verified 2026-05-30).
Architectural rule reuse: pkg/acpserver will follow the pkg/mcpserver invariant (stdlib-only, AST test guard). New components (pkg-acpserver, infra-acp) registered in .go-arch-lint.yml per the repo's architecture rules. Bridge adapters in the interface layer wrap application services with zero infrastructure imports.
F102: ACP Transparent Agent Server
Scope
In Scope
awf acp-serveas the agent subprocesspkg/acpserver/public package: stdlib-only, bidirectional JSON-RPC 2.0 engine with stdio serve loop, 10 MiB scanner ceiling, error code constants, and AST-enforced architecture invariant (mirrorspkg/mcpserver/). Bidirectionality serves only the agent→clientsession/request_permissioncallback in v1 — nofs/+terminal/calls are issued (per decision #2(b))awf acp-servesubcommand withskipFormatValidationannotationinitialize,session/new,session/prompt,session/cancel,session/updatenotifications, with capability negotiationsession/prompttriggers a fullExecutionServicerun, not single-provider passthroughavailable_commands_update);--input=values carried as slash-command argumentsACPInputReaderimplementingports.UserInputReader(channel bridge mirroringtui.TUIInputReader), with turn-boundary resume semantics (one conversation turn = onesession/prompt→session/update* → stopReason cycle)session/request_permissionsession/updateprojection (agent_message_chunk,agent_thought_chunk,tool_call,tool_call_update) via a new ACPRenderFuncsink plugged intoStreamFilterWritertext,resource_link) for prompt ingestionsession/new.mcpServersforwarded into the existing F099 MCP proxyUSER.ACP.*)tool_callID correlationports.ACPClient, applicationACPSessionService, infrastructure adapter,.go-arch-lint.ymlupdated withpkg-acpserver/infra-acpcomponentsOut of Scope
fs/andterminal/client methods (AWF keeps its own builtins + MCP proxy as execution substrate; tool calls are reported only)awf runwith IDE-attach relay (non-standard pattern, deferred)session/load/ session resumesession/set_mode,authenticateflow (no auth methods advertised in v1)Deferred
fs/+terminal/client-capability usesession/loadresumeUser Stories
US1: Editor talks to AWF as a transparent ACP agent (P1 - Must Have)
As a developer using an ACP-compatible editor (Zed, acp.nvim),
I want to connect my editor's agent panel to
awf acp-serveand invoke any AWF workflow as a slash command,So that I get a uniform agent surface in my editor while AWF transparently routes prompts through whichever workflow/provider I configured, with observability, MCP tool interception, and history applied automatically.
Why this priority: This is the entire value proposition — without single-turn workflow execution exposed over ACP, the feature delivers nothing. It is the minimum viable surface that lets an editor drive AWF.
Acceptance Scenarios:
awf acp-serveas an external agent, When the editor sendsinitializefollowed bysession/new, Then AWF responds with negotiatedprotocolVersion, advertisedagentCapabilities, noauthMethods, and anavailable_commands_updatelisting every workflow discovered inconfigs/workflows/as a slash command.session/promptinvoking/my-workflowwith text content blocks, Then AWF executes the full workflow viaExecutionService, projects workflow step structure (atool_call/tool_call_updateper workflow step plus, optionally, aplanfor the step graph), streamsagent_message_chunknotifications for assistant text andtool_call/tool_call_updatefor tool invocations within steps, and terminates the turn withPromptResponse.stopReason = end_turn— i.e. the editor sees multi-step progress, not a single flattened agent stream.--input=name=valueparameters, When the editor sends/my-workflow --input=foo=bar, Then AWF parses slash-command arguments, validates required inputs, and surfaces missing-input errors assession/updatetext + a non-end_turnstop reason.Independent Test: Spawn
awf acp-servevia a stdio harness, send a recordedinitialize+session/new+session/promptJSON-RPC sequence, assert the notification stream contains at least oneagent_message_chunkand the final response carries a validstopReason. Verify command discovery by asserting theavailable_commands_updatenotification names every workflow file present in the fixture directory.US2: Multi-turn conversation with mid-workflow user input (P2 - Should Have)
As a developer driving an AWF workflow that contains conversational agent steps,
I want the workflow to pause for my input at conversation turn boundaries and resume when I send the next
session/prompt,So that I can have a back-and-forth conversation with the underlying agent across multiple ACP turns without restarting the workflow.
Why this priority: AWF's conversational workflows (TUI mode) are a primary differentiator. Without ACP turn-boundary resume, editor users would be locked out of any workflow that uses
ConversationManager.ExecuteConversationwith multi-turn semantics. P2 because US1 is usable for non-conversational workflows on its own.Acceptance Scenarios:
ConversationManagerand callsUserInputReader.ReadInput, When the workflow goroutine blocks onACPInputReader.ReadInput, Then AWF emits the buffered assistant turn, ends the current ACP turn withstopReason = end_turn, and parks the workflow goroutine on the response channel for the lifetime of the session.session/promptwith text content, Then AWF routes the text toACPInputReader.Respond(text), unparks the goroutine (does not start a new workflow), resumes execution, and emits the next assistant turn as a freshsession/updatestream ending in a newstopReason.session/promptwith empty text, Then AWF returnsStopReasonUserExitfrom the conversation loop, allows the workflow to terminate cleanly, and reports the finalstopReasonto the editor.Independent Test: Drive a fixture workflow whose step blocks on user input. Send
session/prompt#1, assert the response stops withend_turnand emits a markeragent_message_chunk. Sendsession/prompt#2 with different text, assertRespondwas invoked (verifiable via instrumentation hook or a stub agent that echoes the input), and that no new workflow was started (workflow start counter unchanged).US3: Cancellation interrupts a running workflow turn (P2 - Should Have)
As a developer running a long workflow from my editor,
I want
session/cancelto interrupt the active turn cleanly without leaking goroutines, subprocesses, or partial state,So that I can recover the editor's agent panel without killing AWF or my session.
Why this priority: Without cancellation, a long or stuck workflow makes the ACP session unusable. P2 because basic execution (US1) and conversation (US2) can ship first; cancellation hardens the surface.
Acceptance Scenarios:
session/promptrunning anExecutionServiceworkflow, When the editor sendssession/cancelwith the activesessionId, Then AWF cancels the execution context, propagates cancellation to all in-flight agent subprocesses (SIGTERM with 5s SIGKILL fallback), and concludes the current turn withstopReason = cancelled.tool_callreported viasession/update, When cancellation completes, Then AWF emits a finaltool_call_updatemarking the tool status ascancelledbefore the prompt response is returned.Independent Test: Start a
session/promptinvoking a workflow with a sleep step. Within 1 second, sendsession/cancel. Assert: response received within 6 seconds (5s SIGTERM grace + 1s),stopReason == cancelled, no orphan subprocesses, no leaked goroutines (verified viago test -race+ leak detector).US4: Tool-call approval gates surface as ACP permission requests (P2 - Should Have)
As a developer with workflows containing approval-gated tool calls,
I want the editor to present the approval dialog natively via
session/request_permission,So that I can allow/deny tool invocations from inside my editor without a separate TUI.
Why this priority: P2 because the routing mechanism is a locked decision (W3 → FR-009 is a MUST): binary approval gates route through native
session/request_permission. The full deny-semantics UX (skip/error/abort) is incremental and builds on AWF's existing approval/interactive step machinery, but the core wiring must ship with the conversational surface.Acceptance Scenarios:
session/request_permissionClient method call with a structured prompt and options (allow,deny), pauses the step, and resumes based on the editor's response.tool_call_update.Independent Test: Run a fixture workflow whose first step is an approval gate. Drive
session/request_permissioncallbacks via a stub ACP client that auto-repliesallowthendeny. Assert each outcome produces the expectedtool_call_updatestatus and that the workflow progresses or terminates as configured.Edge Cases
session/promptwith content blocks AWF does not support (image,audio, embeddedresource)? → Reject with asession/updatetext chunk explaining the unsupported block, end the turn with a non-end_turnstop reason.-32700,-32600,-32601,-32602,-32603) without terminating the serve loop.Delta == truetext fragments? → AWF degrades gracefully, emitting whole-messageagent_message_chunknotifications rather than failing the turn.DisplayEvent.IDfor a tool event? → AWF synthesizes a stable correlation ID per tool invocation so thattool_call_updatenotifications can be matched without provider changes.session/promptarrives while another prompt is in-flight on the samesessionId? → AWF rejects with anINVALID_REQUEST-class error; concurrent prompts on the same session are not supported in v1.ACPInputReader.ReadInputand the editor closes the stdio connection? → AWF cancels the session context, unparks the goroutine with a cancellation signal, and lets the workflow tear down cleanly.session/new.mcpServersis provided alongside a workflow whose steps already configuremcpProxyConfig? → Editor-provided MCP servers are merged into the per-step proxy config with editor entries taking precedence on key collision; document the merge rule in ADR-018.Requirements
Functional Requirements
awf acp-serveCobra subcommand with theskipFormatValidationannotation so--formatvalidation inPersistentPreRundoes not exit the protocol-only command.pkg/acpserver/with stdlib-only imports, enforced by an AST-basedarchitecture_test.goinvariant identical in spirit topkg/mcpserver/.initialize,session/new,session/prompt, andsession/cancelACP Agent methods, plussession/updatenotifications, conforming to a single pinned ACPprotocolVersioninteger documented in ADR-018.DisplayEventvalues produced by every existing per-providerDisplayEventParserinto ACPsession/updatevariants per the mapping:EventTextdelta →agent_message_chunk; reasoning text →agent_thought_chunk;EventToolUsefirst sighting →tool_call; subsequent same-IDEventToolUse→tool_call_update.session/promptas a full workflow execution entry point, invokingExecutionService(theawf runmachinery) rather than a single-provider passthrough.available_commands_updateonsession/new, with--input=key=valuearguments mapped from slash-command arguments and validated against workflow input definitions.ACPInputReaderimplementingports.UserInputReaderthat bridges between the blockingConversationManagergoroutine and the ACP connection:ReadInput(ctx)blocks on a response channel; the next inboundsession/promptcallsRespond(text)to unpark the goroutine; empty text returnsStopReasonUserExit.ReadInputblocks, AWF ends the current ACP turn withstopReason = end_turnand keeps the workflow goroutine parked across ACP turns for the lifetime of the session.session/request_permissionClient-method calls without parking the workflow goroutine on the conversation input channel.session/cancelby cancelling the active execution context, propagating SIGTERM with a 5-second SIGKILL fallback to in-flight agent subprocesses, and terminating the current turn withstopReason = cancelled.session/new.mcpServersinto the per-stepmcpProxyConfigconsumed by the existing F099 MCP proxy, with editor-provided entries taking precedence on key collision. (This merge-precedence rule was introduced by this spec — it resolves research open-question F004: Persistance d'État JSON #6, which was not user-decided — and MUST be ratified in ADR-018.)fs/+terminal/callbacks) in v1, advertising minimal or zerofs+terminalclient-capability needs atinitialize.-32700parse error,-32600invalid request,-32601method not found,-32602invalid params,-32603internal error) for malformed input without terminating the serve loop, and MUST addUSER.ACP.*validation codes mapped to exit code 1 for configuration errors surfaced at startup.ContentBlockvariants (text,resource_link) into the prompt string passed to the workflow; unsupported variants (image,audio, embeddedresource) MUST return a textual rejection notification and a non-end_turnstop reason.pkg-acpserverand the infrastructure ACP adapter in.go-arch-lint.ymlwith dependency rules consistent withpkg-mcpserver/infra-mcp.session/updatenotifications, independently of the per-stepDisplayEventstream of FR-004. The workflow-level projector surfaces the multi-step structure of a workflow run to the editor (e.g. atool_call/tool_call_updateper workflow step, and/or aplanentry describing the step graph) so the editor sees workflow progress rather than a single flattened agent stream. FR-004 (inner agent-step stream) and FR-016 (outer workflow stream) compose: agent text/tool events emitted within a step are correlated under that step's projection. The projector MUST consume the workflow lifecycle / step events thatExecutionServicealready emits through theports.EventPublisherport (workflow.started/completed/failed,step.started/completed/failed/retrying, including per-branch events for parallel steps) — no newExecutionServiceinstrumentation is required (verified 2026-05-30). BecauseExecutionServiceis wired to a singleEventPublisher(the plugin EventBus), the ACP projector MUST be attached via a fan-outEventPublisher(or an EventBus subscription) so it receives events in parallel with plugins without displacing them.Non-Functional Requirements
pkg/acpserver/MUST be stdlib-only with zerointernal/imports, enforced byarchitecture_test.go; the concurrency-heavy ACP serve loop and per-session goroutines MUST reach >85% unit test coverage and passmake test-racewith zero data races. (ACP is JSON-RPC over stdio — there is no gRPC in this feature.)agent_message_chunkto stdout MUST be under 100ms at p95 measured with a synthetic provider stub.bufio.Scannerwith a 10 MiB line ceiling (maxRequestLineBytes) so large payloads (base64 images, big diffs) do not truncate; oversize lines MUST produce a structured error rather than crash.runDryRunorrunInteractive.SECRET_,API_KEY,PASSWORD) MUST also be masked in ACPsession/updatetext projections; ACP wire frames MUST NOT leak masked values.Success Criteria
awf acp-servecan list all configured workflows as slash commands within 1 second ofsession/newcompletion./<workflow-name>from the editor produces the same final output and tool-call trace asawf run <workflow-name>on the CLI, measured against a fixed fixture matrix covering all six providers.session/promptcycles) runs to completion with zero goroutine leaks and zero orphan subprocesses, verified by a race-and-leak integration test.session/cancelreturns control to the editor within 6 seconds at p95 (5s SIGTERM grace + 1s overhead) regardless of which workflow step is active.DisplayEventParseroutputs (covered by*_parse_display_events_test.go) map cleanly onto an ACPsession/updatevariant via the new ACP renderer, asserted by table-driven mapping tests.make build,make lint,make test, andmake test-raceall pass with zero violations after F102 lands; thearchitecture_test.goinvariant blocks anyinternal/import added topkg/acpserver/.Key Entities
ports.UserInputReaderadapter bridgingConversationManagerand the ACP connection via channelsRenderFunc-style sink that convertsDisplayEventstreams intosession/updatenotificationsavailable_commands_updatepkg/acpserver/protocol.gosession/request_permissionsession/request_permissiononly (nofs/+terminal/per decision #2(b))session/updatenotifications; distinct from the per-stepACPRenderer(FR-016)Assumptions
DisplayEventParserpopulatesDisplayEvent.IDconsistently for tool events, or AWF synthesizes a stable per-invocation ID where the provider does not (verified via*_parse_display_events_test.goaudit).available_commands_update(Zed natively does; acp.nvim does).session/promptcalls on the samesessionIdare not required in v1; sequential turn semantics match both the TUI input bridge and the ACPsession/promptlifecycle.TUIInputReaderchannel pattern (internal/interfaces/tui/input_reader.go) is structurally fit for ACP turn-boundary resume; noConversationManagerchanges are required to support a long-parked goroutine across multiple ACP turns.session/new.mcpServerscan be merged into per-stepmcpProxyConfigwithout breaking F099 MCP proxy invariants documented in ADR-017.ExecutionServicealready emits push events for every workflow/step transition via theports.EventPublisherport (emitEvent()→Publish(*pluginmodel.DomainEvent)), including individually-named events for parallel branches. FR-016 is therefore buildable on existing hooks with no newExecutionServiceinstrumentation. The one wiring task is a fan-outEventPublisher:ExecutionServicecurrently accepts a single publisher (the plugin EventBus), so the ACP workflow-event projector must be multiplexed alongside it rather than replacing it. (TUI and HTTP/SSE today pollGetAllStepStates()every 200ms instead of subscribing; the ACP projector should prefer the push path to meet low-latency goals.)Metadata
Dependencies
Clarifications
Session 2026-05-29
awf acp-serve) or CLI-launchedawf runwith IDE-attach relay? → A: IDE-driven (native ACP). CLI-launched + IDE-attach relay is out of scope for v1.Session 2026-05-30
session/prompt— single-provider passthrough or full workflow execution? → A: Full workflow execution viaExecutionService/awf runmachinery. Every step (agent, parallel, terminal), nested conversation, and approval moment projects onto ACP notifications.fs/+terminal/routing — honor editor client methods (a) or keep AWF substrate (b)? → A: (b) AWF substrate. AWF retains its builtins/MCP-proxy as execution substrate and only reports tool calls back assession/update. Option (a) deferred behind a future flag.available_commands_update. Workflow--input=carried as slash-command arguments.session/promptis routed toRespond(text)on the existingACPInputReader, mirroringTUIInputReader. Binary approval gates use nativesession/request_permission.ReadInputends the current ACP turn withstopReason = end_turnand the workflow goroutine stays parked onresponseChfor the lifetime of the session.Notes
pkg/engine and a hidden serve subcommand.USER.ACP.*), decision rationale.protocolVersioninteger from the schema; (2) auditDisplayEvent.IDpopulation across all provider parse tests; (3) ratify thesession/new.mcpServersmerge rule (editor precedence on collision, per FR-011) in ADR-018 and reconcile it with the existingmcpProxyConfigper-step wiring; (4) implement the fan-outEventPublisherand the workflow-levelDomainEvent→session/updateprojector (FR-016) on top of the step/workflow eventsExecutionServicealready emits — noExecutionServiceinstrumentation needed (verified 2026-05-30).pkg/acpserverstdlib infra (serve loop, protocol.go error codes, types.go ACP domain types, architecture_test.go invariant, race tests); Phase 2initialize+session/new+ passthroughsession/promptagainst one provider; Phase 3 streaming viaDisplayEvent→session/updaterenderer; Phase 4 cancel + capabilities + multi-provider; Phase 5 editor integration tests (tests/integration/acp/, fixturestests/fixtures/acp/); Phase 6 stretch (session/load, modes, remote transport).pkg/acpserverwill follow thepkg/mcpserverinvariant (stdlib-only, AST test guard). New components (pkg-acpserver,infra-acp) registered in.go-arch-lint.ymlper the repo's architecture rules. Bridge adapters in the interface layer wrap application services with zero infrastructure imports.pkg/mcpserver/{protocol,server,types,architecture_test}.go;pkg/display/event.go;internal/infrastructure/agents/stream_filter.goand*_provider.go;internal/application/conversation_manager.go;internal/interfaces/cli/mcp_serve.go;internal/interfaces/tui/input_reader.go;internal/domain/ports/agent_provider.go.