Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 6 additions & 5 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

14 changes: 2 additions & 12 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ agentkit-plugins = "=0.10.7"
agentkit-provider-openai = "=0.10.12"
agentkit-provider-openrouter = "=0.10.8"
agentkit-task-manager = "=0.10.7"
agentkit-tool-compose = { version = "=0.10.11", default-features = false, features = ["runlet"] }
agentkit-tool-compose = { version = "=0.10.12", default-features = false, features = ["runlet"] }
agentkit-tool-skills = "=0.10.8"
agentkit-tools-core = "=0.10.5"
arboard = { version = "=3.6.1", default-features = false, features = ["image-data"], optional = true }
Expand Down Expand Up @@ -81,7 +81,7 @@ rpassword = "=7.5.4"
serde = { version = "=1.0.229", features = ["derive"] }
serde_json = "=1.0.151"
shlex = { version = "=2.0.1", optional = true }
runlet = "=0.6.0"
runlet = "=0.6.1"
sha2 = "=0.11.0"
subtle = "=2.6.1"
tar = { version = "=0.4.46", default-features = false }
Expand Down Expand Up @@ -125,7 +125,6 @@ jsonwebtoken = { version = "=11.0.0", default-features = false, features = ["aws
tokio = { version = "=1.53.1", features = ["test-util"] }

[patch.crates-io]
agentkit-provider-openai = { git = "https://git@github.com/danielkov/agentkit.git", rev = "04148b4fe88ebfbe9ba79762dcf272bd5a7b8f58" }
agentkit-loop = { git = "https://github.com/danielkov/agentkit.git", rev = "bec9dcc45ee0f436d538286bc220b16dc19fd5f3" }
agentkit-acp = { git = "https://github.com/daviddanialy/agentkit.git", rev = "6d519ed1e93e28e54ba1cc18889534e2e8337181" }
agent-client-protocol = { git = "https://github.com/danielkov/rust-sdk.git", rev = "423ba77cd555a09f68472b93c142c8f0baaabf43" }
Expand All @@ -137,15 +136,6 @@ agentkit-core = "=0.10.5"
agentkit-task-manager = "=0.10.7"
agentkit-tools-core = "=0.10.5"

# Keep the provider fix on a distinct public HTTPS source so its workspace
# dependencies reuse the existing registry packages and reviewed loop pin.
# The git username distinguishes Cargo source IDs; anonymous fetch still works.
[patch."https://git@github.com/danielkov/agentkit.git"]
agentkit-adapter-completions = "=0.10.9"
agentkit-core = "=0.10.5"
agentkit-http = "=0.10.7"
agentkit-loop = { git = "https://github.com/danielkov/agentkit.git", rev = "bec9dcc45ee0f436d538286bc220b16dc19fd5f3" }

# Deny also in test builds; only test-only scopes may relax ergonomics. Crate
# roots additionally forbid these in production so local allows cannot evade it.
[lints.clippy]
Expand Down
40 changes: 15 additions & 25 deletions docs/user/subagents-and-acp-harnesses.md
Original file line number Diff line number Diff line change
Expand Up @@ -90,38 +90,28 @@ return { main: second.output, alternative: branch.output }

Each successful turn returns a session value with `id`, `name`, `output`, and `generation`. `subagent` creates an ID at generation 1. `prompt` keeps that ID and name while incrementing its generation. `fork` creates a different ID and uses its own preferred or fallback name; its generation is one greater than the supplied source value, and it does not advance the source session. Close a session with either `close(value)` or `close({ id: value.id })`; the latter is useful when only an ID is available. Closing an unknown ID fails with `unknown subagent session`. Kit sends ACP `session/close` when the harness advertises it. Explicit `close` also sends `session/delete` when advertised, removing the discarded branch’s persistent history after closing it. Delete failures are reported rather than silently ignored. Process shutdown and internal cleanup do not delete persistent history; this preserves completed child sessions for restart recovery. A standalone process without that capability is terminated when its handle is dropped. If native-fork siblings share a process and the harness cannot close one logical session, `close` fails rather than claiming success or disrupting the siblings.

Always pass the latest completed value back to `prompt` or `fork`. Reusing an older value fails with `stale subagent generation N; current generation is M`. This prevents two continuations from silently racing on one session. Prompt and fork calls on an individual ACP session are serialized, while separate forked sessions can be prompted concurrently. Steering injects guidance into a working turn without waiting for that turn to finish.
`prompt` accepts `{ id }`, the subagent's ID string, or any value returned for it, and always targets the session's current state. `fork` copies a completed turn, so pass it the latest completed value; an older value fails with `stale subagent generation N; current generation is M`. Prompt and fork calls on an individual ACP session are serialized, while separate forked sessions can be prompted concurrently.

The optional `name` argument is preferred on `subagent` and `fork`; `prompt` has no naming input and preserves the session name. The optional `harness`, `model`, and `cwd` arguments belong only on `subagent`. `harness` overrides the user's configured harness preference. `model` selects an exact model value ID advertised by that harness through its ACP session configuration, or a model alias configured for that harness. `cwd` selects the new subagent's working directory; relative paths resolve from Kit's working directory, and missing paths or non-directories fail before startup. Omit an argument to retain its configured default. `prompt` and `fork` retain the original session's harness, model, and working directory. An explicit model fails before the first prompt if the harness does not advertise a selectable `model` option or rejects the value.

## Steer a working subagent
## Message a subagent in any state

Use `steer({ id, prompt })` to inject guidance into an existing working turn,
without cancelling it or starting a new turn. While the originating `compose`
is backgrounded, call `subagents({})` in a separate compose to find the child's
immutable ID and confirm its status is `working`. Then use that ID:
`prompt({ subagent, prompt })` delivers a message however the subagent's current state allows, and returns the subagent's value once the turn that handled the message ends:

- An idle subagent, including one whose last turn failed, starts a new turn.
- A working subagent receives the message within its current turn when its harness supports ACP v2 `steer` injection. The call then returns that turn's value.
- A working subagent without steering support, or one that is still starting or being forked, receives the message in a new turn after its current work ends.

An `output_schema` applies to a new turn only, so a prompt with one always waits for the current turn to end. While the originating `compose` is backgrounded, `subagents({})` lists the IDs and statuses of working children:

```text
return steer({
id: "s-…",
return prompt({
subagent: { id: "s-…" },
prompt: "Keep the change limited to the parser; do not modify the public API."
})
```

The prompt accepts the same text or ACP content-block input as `prompt`.
Steering requires ACP v2 and a child that advertises `steer` support. Starting,
idle, retired, and fork-reserved sessions are rejected, as are unknown IDs.
Unsupported peers return an error: Kit does not fall back to cancellation or
re-prompting. Use `prompt` with the latest completed handle for an idle child.

The returned value is the child's acceptance receipt, **not proof that the
injection was delivered, applied, or finished**. Steering does not wait for idle
or change the turn, generation, or reusable handle. The original backgrounded
compose remains responsible for returning the completed turn's output. The
child can finish or close between listing and steering, so a working listing
does not guarantee acceptance. If acknowledgement times out, delivery is unknown;
Kit does not cancel the original turn or retry the injection. Dropping the
steering caller also does not revoke an injection already sent to the child.
The prompt accepts text or ACP content blocks. Unknown and retired IDs fail. Cancelling the call stops waiting, but does not revoke a message already injected into a working turn.

## Inspect display names

Expand Down Expand Up @@ -200,7 +190,7 @@ Persistent parent sessions record their direct children’s ACP session IDs, har

Recovery reattaches the same mutable child session, not a snapshot or a new branch. It is not the generic immutable fork fallback discussed in #12. Interrupted turns are not automatically retried, and explicitly closed children are never restored—even if remote history deletion failed. Existing transcripts without child records remain readable but cannot reconstruct their old child handles. New child-state records require a reader that understands the newer transcript schema.

For v2 children, Kit waits for an idle `state_update` after prompt acceptance before returning output. Its stop reason uses the same success, cancellation, refusal, and request-limit handling as v1. Steering requires ACP v2 with advertised `steer` support. An omitted reason permits normal completion; an unknown reason reports an error rather than success. Whole-message updates replace text by message ID; omitted content preserves text, empty or null content clears it, and later chunks append.
For v2 children, Kit waits for an idle `state_update` after prompt acceptance before returning output. Its stop reason uses the same success, cancellation, refusal, and request-limit handling as v1. Delivering a `prompt` into a working turn requires ACP v2 with advertised `steer` support; otherwise the message waits for the next turn. An omitted reason permits normal completion; an unknown reason reports an error rather than success. Whole-message updates replace text by message ID; omitted content preserves text, empty or null content clears it, and later chunks append.

For generic v2 children, recovery uses `session/resume` with `replayFrom: {"type": "start"}`; v1 children must advertise `loadSession` and are reattached with `session/load`. Built-in Kit children use their persistent-session launch path. Replay completes before the next prompt and never replaces the handle’s last-turn output. Kit uses only IDs recorded by the owning parent; it does not scan and adopt unrelated child sessions. If the harness was removed, cannot load sessions, or lost its history, reconnect fails without creating a replacement session. Restore the harness configuration or explicitly close the obsolete handle. Close during an in-progress reconnect reports an error without retiring the handle; retry after startup completes or is cancelled.

Expand Down Expand Up @@ -302,11 +292,11 @@ Nested agents cannot collect interactive input. Kit answers child ACP `elicitati

Cancelling an outer turn propagates to nested work. For a dispatched prompt, Kit sends ACP `session/cancel` and allows up to five seconds for the child to settle; a child that does not settle is terminated. Cancellation while starting, waiting for the session lock, prompting, or forking returns a cancelled tool result. A cancelled fork or a fork that exceeds its 30-second deadline sends `$/cancel_request` for the in-flight request. Kit still waits for a late fork response to clean up any created session before releasing source-session serialization. A `session/close` request that exceeds its five-second deadline also sends `$/cancel_request` before its response is discarded. Protocol cancellation is advisory; it does not guarantee that the child stopped or rolled back the operation.

`end_turn` and `max_tokens` are successful completed turns. In particular, a max-token response returns its partial output and remains reusable. `cancelled`, refusal (`nested agent refused the prompt`), `max_turn_requests` (`nested agent reached its turn-request limit`), protocol errors, and unknown stop reasons are failures. Once a `prompt` continuation has been dispatched and fails, Kit retires that session because its transcript may have changed; retry by starting a new subagent rather than reusing the old value. Reuse can report `unknown subagent session`, `subagent session is retired`, or `nested agent process is no longer running`.
`end_turn` and `max_tokens` are successful completed turns. In particular, a max-token response returns its partial output and remains reusable. `cancelled`, refusal (`nested agent refused the prompt`), `max_turn_requests` (`nested agent reached its turn-request limit`), protocol errors, and unknown stop reasons are failures. A failed turn, including the first turn of a new `subagent` or `fork`, leaves a live child idle with its conversation and edits intact. The error names the subagent's ID, the cause recorded in its fatal error log, and how to continue it with `prompt`. Kit retires a session only when its child process is no longer running; reuse then reports `unknown subagent session`, `subagent session is retired`, or `nested agent process is no longer running`.

## Limits and troubleshooting

Kit currently permits nesting to depth 2 and at most 120 live parent-owned subagent sessions per main session. At the maximum depth, Kit omits `subagent` and `fork` from the compose catalog because neither operation can succeed there; the runtime depth check remains as a fallback. Exceeding the depth or capacity bounds reports `subagent depth limit (2) reached` or `live subagent session limit (120) reached`. Use `subagents({})` to inspect retained sessions and `close` to release sessions that are no longer needed. Closed, failed, or explicitly closed children no longer consume capacity.
Subagents may nest to any depth. At most 120 subagent sessions may be live at once per parent and across a whole delegation tree; exceeding either reports `live subagent session limit (120) reached` or `subagent limit for this delegation tree (120) reached`. Use `subagents({})` to inspect retained sessions and `close` to release sessions that are no longer needed. Closed, failed, or explicitly closed children no longer consume capacity.

ACP startup must complete within 30 seconds. Native `session/fork` must also answer within 30 seconds. Common diagnostics include:

Expand Down
3 changes: 3 additions & 0 deletions fixtures/mock-acp-v2.py
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,9 @@ def prompt(request):
text = selected_models.get(session_id, model_ids[0])
if "MOCK_STRUCTURED_OUTPUT" in text:
text = json.dumps({"approved": True, "reason": "mock approved"})
if "MOCK_TURN_ERROR" in text:
send({"jsonrpc": "2.0", "method": "session/update", "params": {"sessionId": session_id, "update": {"sessionUpdate": "state_update", "state": "idle", "stopReason": "_error"}}})
return
if "MOCK_REFUSAL" in text:
send({"jsonrpc": "2.0", "method": "session/update", "params": {"sessionId": session_id, "update": {"sessionUpdate": "state_update", "state": "idle", "stopReason": "refusal"}}})
return
Expand Down
Loading
Loading