feat(runtime,kernel): non-blocking agent_send via the async-task tracker#6044
Conversation
houko
left a comment
There was a problem hiding this comment.
CHANGELOG.md: kernel(triggers) entry (#5960) was accidentally clobbered
The diff replaced the first sentence of the existing kernel(triggers): TaskClaimed and TaskCompleted triggers gain an optional creator_match filter bullet (#5960, @nevgenov) with the new feat(runtime,kernel) entry, but the second sentence of the kernel(triggers) entry was left in place and is now dangling as a continuation of the async agent_send description.
This line belongs to #5960, not to agent_send:
Accepts an agent UUID, display name, or
"self"; absent (#[serde(default)]) it preserves the legacy fire-for-all behaviour and existing string-form triggers still parse. The original poster is threaded onto theTaskClaimed/TaskCompletedevents from the task record.
The CHANGELOG should read (two separate bullets):
- **feat(runtime,kernel): non-blocking `agent_send` — `"async": true` delegates without blocking the caller** (#6043) (@DaBlitzStein).
`agent_send` was synchronous-only: it awaited the target agent's full reply as the tool result, so any delegation longer than `tool_timeout_secs` (default 120s) was killed mid-flight and the caller's loop stayed blocked the whole time (a Telegram-facing agent went unresponsive while it waited).
Passing `"async": true` now registers the delegation on the existing async-task tracker (#4983, previously wired only to `workflow_start`) via the new `KernelHandle::send_to_agent_async_tracked`, spawns the callee loop detached, and returns a `task_id` immediately. The callee's response is injected back into the caller's session when it finishes — mid-turn if the loop is live, else wake-idle — reusing `TaskKind::Delegation` and `complete_async_task`.
Blocking `agent_send` (the default) is unchanged; the depth guard still rejects too-deep chains before they can fire async.
- **kernel(triggers): `TaskClaimed` and `TaskCompleted` triggers gain an optional `creator_match` filter** (#5960) (@nevgenov), symmetrical to `TaskPosted`'s `assignee_match`, so an orchestrator can scope claim/completion notifications to tasks it originally posted instead of firing on every claim/completion system-wide.
Accepts an agent UUID, display name, or `"self"`; absent (`#[serde(default)]`) it preserves the legacy fire-for-all behaviour and existing string-form triggers still parse. The original poster is threaded onto the `TaskClaimed` / `TaskCompleted` events from the task record.Please restore the kernel(triggers) bullet and remove its dangling sentence from the feat(runtime,kernel) entry.
Generated by Claude Code
ffba027 to
22b538a
Compare
houko
left a comment
There was a problem hiding this comment.
CHANGELOG.md: new entry inserted mid-bullet, corrupting the #5960 entry
The #6043 entry is inserted between the first sentence and the continuation sentence of the TaskClaimed/TaskCompleted trigger filter entry (#5960, @nevgenov).
In the resulting file the #5960 entry reads as a single orphaned sentence, and the Accepts an agent UUID, display name, or \"self"`; absent (`#[serde(default)]`) it preserves the legacy fire-for-all behaviour and existing string-form triggers still parse. The original poster is threaded onto the `TaskClaimed` / `TaskCompleted` events from the task record.continuation — which clearly describes thecreator_matchtrigger filter, notagent_send— ends up visually attached to the new#6043` entry.
The fix is to move the #6043 bullet to a position after the full #5960 entry (i.e., after the Accepts an agent UUID… line), so the TaskClaimed entry stays intact:
- **kernel(triggers): `TaskClaimed` and `TaskCompleted` triggers …** (#5960) (@nevgenov)…
Accepts an agent UUID, display name, or `"self"`; absent (`#[serde(default)]`) it preserves the legacy fire-for-all behaviour and existing string-form triggers still parse. The original poster is threaded onto the `TaskClaimed` / `TaskCompleted` events from the task record.
+- **feat(runtime,kernel): non-blocking `agent_send` …** (#6043) (@DaBlitzStein).
+ …Generated by Claude Code
houko
left a comment
There was a problem hiding this comment.
One CHANGELOG formatting issue to fix before merge (see inline comment on CHANGELOG.md)."
Generated by Claude Code
There was a problem hiding this comment.
The new feat(runtime,kernel) bullet was inserted in the middle of the existing kernel(triggers): TaskClaimed and TaskCompleted entry, splitting it across two separate bullets.
In the diff, the hunk looks like this:
- **kernel(triggers): `TaskClaimed` and `TaskCompleted` … system-wide.
+- **feat(runtime,kernel): non-blocking `agent_send` …
+ …
Accepts an agent UUID, display name, or `"self"`; …
The indented Accepts an agent UUID, display name, or "self"; absent… sentence is the second paragraph of the kernel(triggers) bullet, not a continuation of the new feat(runtime,kernel) entry. As written it now reads as if that paragraph belongs to the async agent_send change, which is incorrect — it describes the creator_match filter for TaskClaimed/TaskCompleted.
Please move the new feat(runtime,kernel) block to after Accepts an agent UUID, display name, or "self"; … so the TaskClaimed/TaskCompleted entry is kept intact.
Generated by Claude Code
22b538a to
3741eb9
Compare
b4e59db to
cfbbc05
Compare
d272241 to
cde9ae0
Compare
cde9ae0 to
dfbd0aa
Compare
…ibrefang#6043) agent_send was synchronous-only: it awaited the target agent's full reply as the tool result, so any delegation longer than tool_timeout_secs (default 120s) was killed mid-flight and the caller's loop stayed blocked the whole time (a channel-facing agent went unresponsive while it waited). Add a non-blocking mode. agent_send with "async": true registers the delegation on the existing async-task tracker (librefang#4983, previously wired only to workflow_start) via the new KernelHandle::send_to_agent_async_tracked, spawns the callee loop detached, and returns a task_id immediately. The callee's response is injected back into the caller's session on completion (mid-turn if the loop is live, else wake-idle), reusing TaskKind::Delegation and complete_async_task. Blocking agent_send (the default) is byte-for-byte unchanged. The inter-agent depth guard still rejects too-deep chains before they can fire async; the synchronous AGENT_CALL_DEPTH scope is intentionally not carried across the detached spawn. Tests: agent_send_async_routes_to_tracked_path_and_returns_task_id and agent_send_async_without_caller_is_invalid_parameter (runtime); the existing kernel TaskKind::Delegation registration/completion tests already cover the injection side.
…im preserves them The ASYNC_RESULT message injected by the async task tracker (librefang#4983) was created as an unpinned user message. When the wake-idle turn fired, the immediate session repair / trim pass could consume the freshly injected message as an orphan or trim candidate, silently dropping the delegation result. Mark it pinned so the repair and trim paths respect it — the same contract used for system constraints and critical context.
… + revert unnecessary pinned When an async agent_send delegation returns a result larger than 500 bytes, the spawn now spills the full response to the artifact store and includes the real handle in the TaskStatus::Completed payload. format_task_completion_text (kernel + runtime, kept in sync) surfaces the handle so the caller can read_artifact the complete content instead of receiving a 300-char truncated preview that provokes hallucinated sha256 hashes. Reverts the pinned:true bandaid on the ASYNC_RESULT injection message (tool_call.rs) — the session repair / trim does NOT consume the message; the earlier failures were caused by Viajecinio's session lock, not by message eviction.
… + forward wake-idle response to home channel - Add chat_id to PendingTask and register_async_task chain - In spawn_wake_idle_turn, resolve home channel and forward the agent response via send_channel_message when chat_id is available - Fall back to user_id when platform_id is empty in build_sender_context so Telegram adapters that don't populate platform_id still get chat_id - Add chat_id parameter to send_to_agent_async_tracked (trait + impl) - Thread chat_id from tool_agent_send through dispatch
b988b62 to
90d5aa9
Compare
houko
left a comment
There was a problem hiding this comment.
CI is failing with 9 compile errors — all in crates/librefang-runtime/src/tool_runner/tests/mod.rs. Two root causes:
1. DispatchCapture::send_to_agent_async_tracked is missing the chat_id parameter (E0050)
The trait signature in agent_control.rs was extended to include chat_id: Option<&str> as the last parameter, but the test mock at line 654 only has 5 parameters (excluding &self). Fix:
async fn send_to_agent_async_tracked(
&self,
_agent_id: &str,
_message: &str,
_caller_agent_id: &str,
caller_session_id: Option<&str>,
_conversation_key: Option<&str>,
+ _chat_id: Option<&str>,
) -> Result<String, librefang_kernel_handle::KernelOpError> {2. Eight test call sites pass 4 arguments to tool_agent_send but the function now takes 5 (E0061)
tool_agent_send gained chat_id: Option<&str> as a fifth parameter, but the test updates only added caller_session_id. Each call needs one more trailing None:
line 856: tool_agent_send(&input, Some(&kernel), None, None) → …, None, None, None)
line 872: tool_agent_send(&input, Some(&kernel), Some("parent-agent"), None) → …, None, None)
line 893: same
line 902: same
line 929: same
line 964: tool_agent_send(&input, Some(&kernel), Some("parent-agent"), Some(session)) → …, Some(session), None)
line 1001: tool_agent_send(&input, Some(&kernel), None, None) → …, None, None, None)
line 1034: tool_agent_send(&input, Some(&kernel), Some("parent-agent"), None) → …, None, None)
No logic changes required — just the trailing None (tests have no channel context, so None is correct). cargo check -p librefang-runtime will be green after these edits.
Generated by Claude Code
houko
left a comment
There was a problem hiding this comment.
Requesting changes — the branch does not compile (CI red on Build / Linux aarch64, Test / Unit, Test / Ubuntu shard 1/4, and Quality).
Root cause from the build logs (run 27844448423), and these are internal to this PR — send_to_agent_async_tracked is newly introduced here and the PR is only 1 commit behind main, so it's not a rebase/drift issue:
error[E0050]: method 'send_to_agent_async_tracked' has 6 parameters but the declaration in trait 'send_to_agent_async_tracked' has 7— the trait declaration and its impl disagree on arity; the signature was changed in one place but not the other.- Multiple
error[E0061]: this function takes 5 arguments but 4 arguments were supplied— call sites weren't updated to the new signature.
Please reconcile the trait/impl signature and update every call site so it compiles, then push — happy to re-review once CI is green. The non-blocking-agent_send direction itself looks right; this is purely a half-applied signature change.
The production trait/impl and tool_agent_send gained a chat_id: Option<&str> parameter, but the mock impl of send_to_agent_async_tracked and the 8 tool_agent_send callsites in tool_runner/tests/mod.rs were not updated, breaking the build (E0050 + E0061). Adds the missing chat_id param to the mock and None as the chat_id argument at each callsite. Test-only; no production change.
|
Pushed a small test-only fix (f1e1bca) to get the build green. The production side (trait, real impl, the
Added the missing |
…g#6043) register_async_task gained a chat_id: Option<String> parameter; the 4 callsites in async_task_tracker_runtime_test.rs passed only 3 args (E0061). Add None (these tests exercise no chat routing). Test-only.
…fang#6043) The kernel-side async_task_tracker tests had 16 register_async_task callsites passing 3 args; the method now takes a 4th chat_id: Option<String>. Add None (these tests exercise no chat routing). Test-only.
The added None argument pushed 8 inline register_async_task / tool_agent_send calls over rustfmt's width limit; wrap them per rustfmt.
…g#6043) clippy::needless_borrow — Some(ref cid) on chat_id.as_ref() binds &&String. Use Some(cid) + cid.clone().
Summary
Closes #6043.
agent_sendwas synchronous-only:tool_agent_sendawaited the target agent's full reply as the tool result, so any delegation longer thantool_timeout_secs(default 120s) was killed mid-flight, and the caller's loop stayed blocked the whole time. On production this showed up as a Telegram-facing agent going unresponsive while a research sub-agent ran, then failing withTool execution timed out after 120s tool=agent_send.This extends the existing async-task tracker (#4983 — previously wired only to
workflow_start) to inter-agent delegation. No new transport: it reusesregister_async_task/complete_async_taskand the already-presentTaskKind::Delegation.Change
KernelHandle::send_to_agent_async_tracked(crates/librefang-kernel-handle/src/agent_control.rs) with a default impl that falls back to blockingsend_to_agent_asfor handles without a tracker (mocks/tests).crates/librefang-kernel/src/kernel/handles/agent_control.rs) mirrorsstart_workflow_async_tracked: resolves target + caller, registers aTaskKind::Delegation { agent_id, prompt_hash }, spawns the callee loop detached viaself_handle, and on completion callscomplete_async_taskwith the reply — which the tracker injects back into the caller's session (mid-turnAgentLoopSignal::TaskCompleted, else wake-idle). Falls back to a blocking send when no caller session is parseable (so the result is never silently dropped).tool_runner/agent.rs):agent_sendgains an"async": booleanparam (default false). When true it routes to the tracked path and returns{ "task_id", "status": "delegated", "note" }immediately. The depth guard still runs first; the synchronousAGENT_CALL_DEPTHscope is intentionally not carried across the detached spawn.caller_session_idis now threaded fromdispatch.rs.async).Blocking
agent_send(the default) is byte-for-byte unchanged.Verification
cargo check --workspace --lib— clean.cargo clippy -p librefang-kernel-handle -p librefang-kernel -p librefang-runtime --all-targets -- -D warnings— clean.cargo test -p librefang-runtime --lib tool_runner::tests— 237 passed (incl. 2 new:agent_send_async_routes_to_tracked_path_and_returns_task_id,agent_send_async_without_caller_is_invalid_parameter).cargo test -p librefang-kernel --test async_task_tracker_test— 15 passed (existingTaskKind::Delegationregistration / dedup / completion-injection coverage already exercises the delivery side this PR drives).Notes / follow-ups
agent_message_receivedtrigger so a recipient owns its reaction, generalisingtask_posted) was discussed as a separate, larger design — not in scope here.prompt_hashusesDefaultHasher(opaque, caller's-choice per the field docs); swap to a stable hash if cross-process dedup of delegations is ever needed.