commit 11cb7bb8d4c70c4973e2647be12cfd08cadbc8df from: Isaac Meerleo date: Wed Jul 15 16:55:15 2026 UTC integrate canonical generation into subagents commit - 91e58933b7fe2e7ffb5f1943b2bdada616824b69 commit + 11cb7bb8d4c70c4973e2647be12cfd08cadbc8df blob - 108c066fffece98abe222353b3520c7fbb28d169 blob + 401a79bf7f548c9d585d90ad873ad32d7b0357fe --- regress/turn/agent.sh +++ regress/turn/agent.sh @@ -465,6 +465,61 @@ ok "pool: exactly eight requests back off and retry on [ "$nretry" -eq 8 ]; echo $?) stop_stub +# A complete but non-object tool input invalidates the child's Generation. +# It must fail before accepting the assistant or starting a tool/follow-up +# Generation; the Lead receives the bounded typed failure directly. +rm -rf "$HOME/.fugu" +start_stub AGENTCALL BADARGS BADARGSSYNTH BADARGSUNUSED +write_conf +run_fugu "exercise malformed subagent tool arguments" +ok "malformed arguments: lead turn completes" $([ "$rc" -eq 0 ]; echo $?) +n=$(grep -c '^--- .* ---$' "$dir/reqlog") +ok "malformed arguments: no child follow-up generation ran" \ + $([ "$n" -eq 3 ]; echo $?) +request_section BADARGSSYNTH "$dir/badargs-lead" +grep -q 'subagent provider error' "$dir/badargs-lead" && \ + grep -q 'malformed tool arguments' "$dir/badargs-lead" && \ + grep -q '"is_error":true' "$dir/badargs-lead" +ok "malformed arguments: invalid Generation returns a tool error" $? +grep -q 'agent\[reader\]: tool read' "$dir/errout" 2>/dev/null +ok "malformed arguments: rejected call is never executed" \ + $([ $? -ne 0 ]; echo $?) +stop_stub + +# A normal provider stop with neither text nor tools is not a successful +# empty report. It is an invalid Generation returned to the Lead as an error. +rm -rf "$HOME/.fugu" +start_stub AGENTCALL EMPTY EMPTYSYNTH +write_conf +run_fugu "exercise an empty normal subagent Generation" +ok "empty Generation: lead turn completes" $([ "$rc" -eq 0 ]; echo $?) +n=$(grep -c '^--- .* ---$' "$dir/reqlog") +ok "empty Generation: exactly one child request ran" \ + $([ "$n" -eq 3 ]; echo $?) +request_section EMPTYSYNTH "$dir/empty-lead" +grep -q 'subagent provider error' "$dir/empty-lead" && \ + grep -q 'provider sent an empty reply' "$dir/empty-lead" && \ + grep -q '"is_error":true' "$dir/empty-lead" +ok "empty Generation: invalid reply returns a tool error" $? +stop_stub + +# An absent no-argument input is canonicalized to an object before both the +# assistant Projection and execution, preserving a structurally valid pair. +rm -rf "$HOME/.fugu" +start_stub AGENTCALL NOARGS NOARGSREPORT NOARGSSYNTH +write_conf +run_fugu "exercise an absent subagent tool input" +ok "absent arguments: lead turn completes" $([ "$rc" -eq 0 ]; echo $?) +request_section NOARGSREPORT "$dir/noargs-followup" +grep -q '"id":"toolu_noargs"' "$dir/noargs-followup" && \ + grep -q '"name":"ls"' "$dir/noargs-followup" && \ + grep -q '"input":{}' "$dir/noargs-followup" && \ + grep -q '"tool_use_id":"toolu_noargs"' "$dir/noargs-followup" +ok "absent arguments: canonical object and result are paired" $? +grep -q 'agent\[reader\]: tool ls' "$dir/errout" +ok "absent arguments: canonical object is executable" $? +stop_stub + # A token-limited text reply is not mistaken for a completed report. Its # partial text is preserved, followed by one tool-free conclusion request. rm -rf "$HOME/.fugu" blob - 9697df6b559601e6b8fc3a9031bb897f6d7d91ec blob + 66ec0c8fa2bdac78fb1dc2dd24b423fb2e70e562 --- regress/turn/stub.c +++ regress/turn/stub.c @@ -22,7 +22,9 @@ * error event), HTTP500 (a non-2xx before any stream), HTTP401 (an * Anthropic-style JSON error body), RETRY503/RETRY429 (retryable * statuses, 429 carrying Retry-After: 2), EMPTY (a well-formed - * stream with no content blocks), WEBFETCH (a brokered web_fetch), + * stream with no content blocks), BADARGS (a complete but malformed + * subagent tool input), NOARGS (a subagent tool call with absent input), + * WEBFETCH (a brokered web_fetch), * BGTOOL (shell_bg sleep 300), ERROR_BG (the provider failure after * that launch), JOBOUTPUT (shell_output job 1), DOUBLE (a hostile error event * appended after a complete OK stream), AGENTCALL (lead delegates to a @@ -910,6 +912,43 @@ static const char sse_empty[] = "event: message_stop\n" "data: {\"type\":\"message_stop\"}\n\n"; +/* A complete tool call whose argument bytes are not a JSON object. */ +static const char sse_badargs[] = +"event: message_start\n" +"data: {\"type\":\"message_start\",\"message\":{\"usage\":" + "{\"input_tokens\":11,\"output_tokens\":1}}}\n\n" +"event: content_block_start\n" +"data: {\"type\":\"content_block_start\",\"index\":0," + "\"content_block\":{\"type\":\"tool_use\",\"id\":" + "\"toolu_badargs\",\"name\":\"read\"}}\n\n" +"event: content_block_delta\n" +"data: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":" + "{\"type\":\"input_json_delta\",\"partial_json\":\"[1]\"}}\n\n" +"event: content_block_stop\n" +"data: {\"type\":\"content_block_stop\",\"index\":0}\n\n" +"event: message_delta\n" +"data: {\"type\":\"message_delta\",\"delta\":{\"stop_reason\":" + "\"tool_use\"},\"usage\":{\"output_tokens\":1}}\n\n" +"event: message_stop\n" +"data: {\"type\":\"message_stop\"}\n\n"; + +/* An optional-argument tool call with no input_json_delta at all. */ +static const char sse_noargs[] = +"event: message_start\n" +"data: {\"type\":\"message_start\",\"message\":{\"usage\":" + "{\"input_tokens\":11,\"output_tokens\":1}}}\n\n" +"event: content_block_start\n" +"data: {\"type\":\"content_block_start\",\"index\":0," + "\"content_block\":{\"type\":\"tool_use\",\"id\":" + "\"toolu_noargs\",\"name\":\"ls\"}}\n\n" +"event: content_block_stop\n" +"data: {\"type\":\"content_block_stop\",\"index\":0}\n\n" +"event: message_delta\n" +"data: {\"type\":\"message_delta\",\"delta\":{\"stop_reason\":" + "\"tool_use\"},\"usage\":{\"output_tokens\":1}}\n\n" +"event: message_stop\n" +"data: {\"type\":\"message_stop\"}\n\n"; + static const char http500[] = "HTTP/1.1 500 Internal Server Error\r\n" "content-length: 21\r\n\r\n" @@ -1361,6 +1400,16 @@ main(int argc, char *argv[]) write_chunk(conn, sse_agentreport, strlen(sse_agentreport)); (void)tls_write_all(conn, "0\r\n\r\n", 5); + } else if (strcmp(argv[i], "BADARGSSYNTH") == 0) { + send_text(conn, "BADARGS-SYNTHESIS-MARKER"); + } else if (strcmp(argv[i], "BADARGSUNUSED") == 0) { + send_text(conn, "BADARGS-OLD-FOLLOWUP-MARKER"); + } else if (strcmp(argv[i], "EMPTYSYNTH") == 0) { + send_text(conn, "EMPTY-SYNTHESIS-MARKER"); + } else if (strcmp(argv[i], "NOARGSREPORT") == 0) { + send_text(conn, "NOARGS-CHILD-REPORT"); + } else if (strcmp(argv[i], "NOARGSSYNTH") == 0) { + send_text(conn, "NOARGS-SYNTHESIS-MARKER"); } else if (strcmp(argv[i], "FUTUREREPORT") == 0) { send_text(conn, "FUTURE-LITERAL-PROTECTED-REPORT"); } else if (strcmp(argv[i], "FUTURESYNTH") == 0) { @@ -1644,6 +1693,18 @@ main(int argc, char *argv[]) write_chunk(conn, sse_empty, strlen(sse_empty)); (void)tls_write_all(conn, "0\r\n\r\n", 5); + } else if (strcmp(argv[i], "BADARGS") == 0) { + (void)tls_write_all(conn, body_ok, + strlen(body_ok)); + write_chunk(conn, sse_badargs, + strlen(sse_badargs)); + (void)tls_write_all(conn, "0\r\n\r\n", 5); + } else if (strcmp(argv[i], "NOARGS") == 0) { + (void)tls_write_all(conn, body_ok, + strlen(body_ok)); + write_chunk(conn, sse_noargs, + strlen(sse_noargs)); + (void)tls_write_all(conn, "0\r\n\r\n", 5); } else if (strcmp(argv[i], "DOUBLE") == 0) { /* a hostile terminal after the terminal */ (void)tls_write_all(conn, body_ok, blob - 1a4fe85c811b6807de7951646229f2ad3e684147 blob + fb7083b3591be50cbf6ff1e8e15d8630a1c70563 --- src/fugu-tool/agent.c +++ src/fugu-tool/agent.c @@ -38,7 +38,7 @@ #include "log.h" #include "xmalloc.h" #include "buf.h" -#include "json.h" +#include "generation.h" #include "msg.h" #include "anthropic.h" #include "openai.h" @@ -48,37 +48,19 @@ #include "agent.h" #define AGENT_ITER_MAX 25 -#define AGENT_CALL_MAX 64 #define AGENT_PROGRESS_MAX 256 -static const char agent_limited_empty[] = - "Generation stopped before emitting content."; - #define OBJ(props, req) "{\"type\":\"object\",\"properties\":" props \ ",\"required\":" req "}" -struct agent_tcall { - int index; - char id[256]; - char name[FUGU_TOOL_NAME_MAX]; - struct buf input; - size_t input_total; - int malformed; -}; - struct agent_state { struct worker *w; struct tool_ctx *tools; struct agent_boot *boot; struct msglist conv; int prev_msgs; - struct buf text; struct buf report; - struct agent_tcall calls[AGENT_CALL_MAX]; - int ncalls; - char stop[FUGU_STOP_REASON_MAX]; char err[FUGU_ERROR_MAX]; - size_t arg_total; size_t data_total; int data_exceeded; int iterations; @@ -363,28 +345,6 @@ agent_build_request(struct agent_state *a, struct buf } } -static struct agent_tcall * -agent_call_find(struct agent_state *a, int index) -{ - int i; - - for (i = 0; i < a->ncalls; i++) - if (a->calls[i].index == index) - return (&a->calls[i]); - return (NULL); -} - -static void -agent_calls_reset(struct agent_state *a) -{ - int i; - - for (i = 0; i < a->ncalls; i++) - buf_free(&a->calls[i].input); - a->ncalls = 0; - a->arg_total = 0; -} - static int agent_account(struct agent_state *a, size_t len) { @@ -396,21 +356,78 @@ agent_account(struct agent_state *a, size_t len) return (0); } +static enum generation_kind +agent_generation_kind(uint32_t type) +{ + switch (type) { + case FUGU_IMSG_A_TEXT: + return (GENERATION_TEXT); + case FUGU_IMSG_A_TOOL_BEGIN: + return (GENERATION_TOOL_BEGIN); + case FUGU_IMSG_A_TOOL_INPUT: + return (GENERATION_TOOL_INPUT); + case FUGU_IMSG_A_USAGE: + return (GENERATION_USAGE); + case FUGU_IMSG_A_RETRY: + return (GENERATION_RETRY); + case FUGU_IMSG_A_DONE: + return (GENERATION_DONE); + case FUGU_IMSG_A_ERROR: + return (GENERATION_ERROR); + default: + fatalx("subagent unexpected imsg %u", type); + } +} + +static void +agent_invalid_error(struct agent_state *a, + enum generation_invalid_cause cause) +{ + const char *s; + + switch (cause) { + case GENERATION_INVALID_DATA_BOUND: + s = "subagent data bound exceeded"; + break; + case GENERATION_INVALID_USAGE_BOUND: + s = "subagent usage bound exceeded"; + break; + case GENERATION_INVALID_TOOL_CALL_BOUND: + s = "subagent tool-call bound exceeded"; + break; + case GENERATION_INVALID_EMPTY_REPLY: + s = "provider sent an empty reply"; + break; + case GENERATION_INVALID_TOOL_ARGUMENTS: + s = "provider sent malformed tool arguments"; + break; + case GENERATION_INVALID_NONE: + default: + fatalx("subagent invalid Generation without a cause"); + } + strlcpy(a->err, s, sizeof(a->err)); +} + /* Send one projection and collect its already-decoded provider events. */ static int agent_generation(struct agent_state *a, const struct tool_def *defs, - int ndefs) + int ndefs, struct generation_result *result) { - struct buf body; - struct imsg imsg; - struct imsgev_payload payload; - const u_char *p; - size_t len; - uint32_t type; + struct a_usage au; + struct buf body; + struct generation *g; + struct generation_event ev; + struct generation_usage gu; + struct imsg imsg; + struct imsgev_payload payload; + const void *feed_data; + const u_char *p; + size_t feed_len, len; + uint32_t type; + enum generation_feed_status status; + enum generation_kind kind; - agent_calls_reset(a); - buf_reset(&a->text); - a->stop[0] = '\0'; + memset(result, 0, sizeof(*result)); a->err[0] = '\0'; buf_init(&body); agent_build_request(a, &body, defs, ndefs); @@ -419,6 +436,7 @@ agent_generation(struct agent_state *a, const struct t strlcpy(a->err, "subagent request exceeds 16 MiB", sizeof(a->err)); return (-1); } + g = generation_new(FUGU_TURN_DATA_MAX - a->data_total); agent_progress(a, "requesting %s", a->boot->model); agent_send_bytes(a, FUGU_IMSG_AGENT_REQUEST, body.data, body.len); agent_send(a, FUGU_IMSG_AGENT_REQUEST_END, NULL, 0); @@ -431,103 +449,74 @@ agent_generation(struct agent_state *a, const struct t fatal("get subagent provider payload"); p = payload.data; len = payload.len; - switch (type) { - case FUGU_IMSG_A_TEXT: - if (len > FUGU_RESPONSE_MAX - a->text.len) { - strlcpy(a->err, "subagent response exceeds 16 MiB", - sizeof(a->err)); - imsg_free(&imsg); - return (-1); - } - if (p != NULL && len > 0 && - agent_account(a, len) == 0) - buf_add(&a->text, p, len); - break; - case FUGU_IMSG_A_TOOL_BEGIN: { - struct a_tool_begin h; - struct agent_tcall *tc; - const char *id, *name; - size_t il, nl; - - if (p == NULL || len < sizeof(h) || - a->ncalls >= AGENT_CALL_MAX) - fatalx("subagent malformed tool begin"); - memcpy(&h, p, sizeof(h)); - id = (const char *)p + sizeof(h); - il = strnlen(id, len - sizeof(h)); - if (il == 0 || il == len - sizeof(h) || il >= 256) - fatalx("subagent malformed tool id"); - name = id + il + 1; - nl = len - sizeof(h) - il - 1; - if (h.index < 0 || agent_call_find(a, h.index) != NULL || - nl == 0 || nl >= FUGU_TOOL_NAME_MAX || - memchr(name, '\0', nl) != NULL) - fatalx("subagent malformed tool name"); - tc = &a->calls[a->ncalls++]; - memset(tc, 0, sizeof(*tc)); - tc->index = h.index; - memcpy(tc->id, id, il + 1); - memcpy(tc->name, name, nl); - tc->name[nl] = '\0'; - buf_init(&tc->input); - break; + if (type == FUGU_IMSG_SHUTDOWN) { + imsg_free(&imsg); + exit(0); } - case FUGU_IMSG_A_TOOL_INPUT: { - struct agent_tcall *tc; - int idx; + kind = agent_generation_kind(type); + feed_data = p; + feed_len = len; + if (kind == GENERATION_USAGE && p != NULL && + len == sizeof(au)) { + memcpy(&au, p, sizeof(au)); + memset(&gu, 0, sizeof(gu)); + gu.input_tokens = au.input_tokens; + gu.output_tokens = au.output_tokens; + gu.cache_read = au.cache_read; + gu.cache_write = au.cache_write; + feed_data = &gu; + feed_len = sizeof(gu); + } + status = generation_feed(g, kind, feed_data, feed_len, &ev); + if (status == GENERATION_PROTOCOL) { + const char *why = generation_error(g); - if (p == NULL || len < sizeof(idx)) - fatalx("subagent malformed tool input"); - memcpy(&idx, p, sizeof(idx)); - tc = agent_call_find(a, idx); - if (tc == NULL || len - sizeof(idx) > - FUGU_TOOL_ARG_MAX - tc->input_total || len - sizeof(idx) > - FUGU_TOOL_ARGS_TOTAL_MAX - a->arg_total) - fatalx("subagent excessive tool input"); - len -= sizeof(idx); - if (agent_account(a, len) == 0) - buf_add(&tc->input, p + sizeof(idx), len); - tc->input_total += len; - a->arg_total += len; - break; + imsg_free(&imsg); + fatalx("subagent provider protocol: %s", + why != NULL ? why : "invalid Generation event"); } - case FUGU_IMSG_A_USAGE: - if (p == NULL || len != sizeof(struct a_usage)) - fatalx("subagent malformed usage"); - agent_send(a, FUGU_IMSG_AGENT_USAGE, p, len); - break; - case FUGU_IMSG_A_RETRY: + if (status == GENERATION_MORE && kind == GENERATION_USAGE) { + memset(&au, 0, sizeof(au)); + au.input_tokens = ev.usage.input_tokens; + au.output_tokens = ev.usage.output_tokens; + au.cache_read = ev.usage.cache_read; + au.cache_write = ev.usage.cache_write; + agent_send(a, FUGU_IMSG_AGENT_USAGE, &au, sizeof(au)); + } else if (status == GENERATION_MORE && + kind == GENERATION_RETRY) agent_progress(a, "provider retry"); - break; - case FUGU_IMSG_A_DONE: - if (p == NULL || len == 0 || len >= sizeof(a->stop) || - memchr(p, '\0', len) != NULL) - fatalx("subagent malformed stop reason"); - memcpy(a->stop, p, len); - a->stop[len] = '\0'; - imsg_free(&imsg); - if (a->data_exceeded) { - strlcpy(a->err, "subagent data bound exceeded", - sizeof(a->err)); - return (-1); - } + imsg_free(&imsg); + if (status == GENERATION_MORE || status == GENERATION_DRAIN) + continue; + if (status != GENERATION_TERMINAL) + fatalx("subagent Generation returned an unknown feed status"); + if (generation_take(g, result) == -1) + fatalx("subagent could not take terminal Generation"); + if (result->data_used > FUGU_TURN_DATA_MAX - a->data_total) + fatalx("subagent Generation violated its data budget"); + a->data_total += result->data_used; + if (result->outcome == GENERATION_NORMAL || + result->outcome == GENERATION_LENGTH) { + if (result->assistant == NULL || + result->invalid_cause != GENERATION_INVALID_NONE) + fatalx("successful subagent Generation has no assistant"); + result->reason = NULL; + generation_free(g); return (0); - case FUGU_IMSG_A_ERROR: - if (p == NULL || len == 0 || len >= sizeof(a->err) || - memchr(p, '\0', len) != NULL) - fatalx("subagent malformed provider error"); - memcpy(a->err, p, len); - a->err[len] = '\0'; - imsg_free(&imsg); - return (-1); - case FUGU_IMSG_SHUTDOWN: - imsg_free(&imsg); - exit(0); - default: - fatalx("subagent unexpected imsg %u", type); } - imsg_free(&imsg); + if (result->assistant != NULL) + fatalx("failed subagent Generation returned an assistant"); + if (result->outcome == GENERATION_PROVIDER_ERROR) + strlcpy(a->err, result->reason, sizeof(a->err)); + else if (result->outcome == GENERATION_INVALID) + agent_invalid_error(a, result->invalid_cause); + else + fatalx("subagent Generation returned an unknown outcome"); + result->reason = NULL; + generation_free(g); + return (-1); } + generation_free(g); strlcpy(a->err, "coordinator closed the subagent channel", sizeof(a->err)); return (-1); } @@ -549,7 +538,7 @@ agent_local_allowed(const char *name) } static void -agent_web(struct agent_state *a, struct agent_tcall *tc, struct buf *out, +agent_web(struct agent_state *a, const struct block *call, struct buf *out, int *is_error) { struct tool_req req; @@ -560,10 +549,10 @@ agent_web(struct agent_state *a, struct agent_tcall *t size_t len; memset(&req, 0, sizeof(req)); - strlcpy(req.name, tc->name, sizeof(req.name)); + strlcpy(req.name, call->tool_name, sizeof(req.name)); agent_send(a, FUGU_IMSG_AGENT_WEB_REQ, &req, sizeof(req)); - agent_send_bytes(a, FUGU_IMSG_AGENT_WEB_ARG, tc->input.data, - tc->input.len); + agent_send_bytes(a, FUGU_IMSG_AGENT_WEB_ARG, call->tool_input, + call->tool_input_len); agent_send(a, FUGU_IMSG_AGENT_WEB_RUN, NULL, 0); buf_reset(out); *is_error = 1; @@ -599,35 +588,6 @@ agent_web(struct agent_state *a, struct agent_tcall *t exit(0); } -static struct msg * -agent_assistant_message(struct agent_state *a) -{ - struct msg *m; - int i; - - m = msg_new(ROLE_ASSISTANT); - if (a->text.len > 0) - msg_add_text(m, (const char *)a->text.data, a->text.len); - else if (a->ncalls == 0 && - (strcmp(a->stop, "max_tokens") == 0 || - strcmp(a->stop, "length") == 0)) - msg_add_text(m, agent_limited_empty, - sizeof(agent_limited_empty) - 1); - for (i = 0; i < a->ncalls; i++) { - if (a->calls[i].input.len == 0) - buf_addstr(&a->calls[i].input, "{}"); - else if (!json_valid_object(a->calls[i].input.data, - a->calls[i].input.len)) { - buf_reset(&a->calls[i].input); - buf_addstr(&a->calls[i].input, "{}"); - a->calls[i].malformed = 1; - } - msg_add_tool_use(m, a->calls[i].id, a->calls[i].name, - (const char *)a->calls[i].input.data, a->calls[i].input.len); - } - return (m); -} - static void agent_account_result(struct agent_state *a, struct buf *out, int *is_error) { @@ -652,89 +612,86 @@ agent_account_result(struct agent_state *a, struct buf } static struct msg * -agent_execute_calls(struct agent_state *a) +agent_execute_calls(struct agent_state *a, const struct msg *assistant) { - struct msg *m; - struct buf out; - int i, is_error; + struct block *call; + struct msg *m; + struct buf out; + int is_error; m = msg_new(ROLE_USER); buf_init(&out); - for (i = 0; i < a->ncalls; i++) { - struct agent_tcall *tc = &a->calls[i]; - + TAILQ_FOREACH(call, &assistant->blocks, entry) { + if (call->type != BLOCK_TOOL_USE) + continue; if (a->data_exceeded) { buf_reset(&out); buf_addstr(&out, "subagent data bound exceeded"); is_error = 1; } - if (!a->data_exceeded && tc->malformed) { - buf_reset(&out); - buf_addstr(&out, "subagent: malformed tool arguments"); - is_error = 1; - } else if (!a->data_exceeded && agent_local_allowed(tc->name)) { - agent_progress(a, "tool %s", tc->name); - tool_exec(a->tools, tc->name, tc->input.data, tc->input.len, - &out, &is_error); - } else if (!a->data_exceeded && agent_is_web(tc->name) && - ((strcmp(tc->name, "http_request") == 0 && + if (!a->data_exceeded && agent_local_allowed(call->tool_name)) { + agent_progress(a, "tool %s", call->tool_name); + tool_exec(a->tools, call->tool_name, call->tool_input, + call->tool_input_len, &out, &is_error); + } else if (!a->data_exceeded && agent_is_web(call->tool_name) && + ((strcmp(call->tool_name, "http_request") == 0 && a->boot->start.http_request) || - (strcmp(tc->name, "http_request") != 0 && + (strcmp(call->tool_name, "http_request") != 0 && a->boot->start.web_search))) { - agent_progress(a, "tool %s", tc->name); - agent_web(a, tc, &out, &is_error); + agent_progress(a, "tool %s", call->tool_name); + agent_web(a, call, &out, &is_error); } else if (!a->data_exceeded) { buf_reset(&out); buf_addf(&out, "%s is unavailable to a read-only subagent", - tc->name); + call->tool_name); is_error = 1; } agent_account_result(a, &out, &is_error); - msg_add_tool_result(m, tc->id, (const char *)out.data, out.len, - is_error); + msg_add_tool_result(m, call->tool_id, (const char *)out.data, + out.len, is_error); } buf_free(&out); return (m); } static struct msg * -agent_answer_dangling(struct agent_state *a) +agent_answer_dangling(struct agent_state *a, const struct msg *assistant) { static const char reason[] = "subagent tool call was truncated and was not executed"; - struct msg *m; - struct buf out; - int i, is_error; + struct block *call; + struct msg *m; + struct buf out; + int is_error; m = msg_new(ROLE_USER); buf_init(&out); - for (i = 0; i < a->ncalls; i++) { + TAILQ_FOREACH(call, &assistant->blocks, entry) { + if (call->type != BLOCK_TOOL_USE) + continue; buf_reset(&out); buf_add(&out, reason, sizeof(reason) - 1); is_error = 1; agent_account_result(a, &out, &is_error); - msg_add_tool_result(m, a->calls[i].id, + msg_add_tool_result(m, call->tool_id, (const char *)out.data, out.len, is_error); } buf_free(&out); return (m); } -static int -agent_stop_limited(const struct agent_state *a) -{ - return (strcmp(a->stop, "max_tokens") == 0 || - strcmp(a->stop, "length") == 0); -} - static void -agent_report_text(struct agent_state *a) +agent_report_text(struct agent_state *a, const struct msg *assistant) { - if (a->text.len == 0) - return; - if (a->report.len > 0) - buf_addstr(&a->report, "\n\n"); - buf_add(&a->report, a->text.data, a->text.len); + struct block *text; + + TAILQ_FOREACH(text, &assistant->blocks, entry) { + if (text->type != BLOCK_TEXT || text->textlen == 0) + continue; + if (a->report.len > 0) + buf_addstr(&a->report, "\n\n"); + buf_add(&a->report, text->text, text->textlen); + } } static void @@ -802,12 +759,14 @@ agent_conclude(struct agent_state *a, struct msg *m) "execution bound. Do not call tools. " "Conclude now with the most useful concise final report possible, " "including uncertainty and unfinished checks."; + struct generation_result result; msg_add_text(m, conclude, sizeof(conclude) - 1); agent_report_tail(a); - if (agent_generation(a, NULL, 0) == 0) - agent_report_text(a); - else if (a->err[0] != '\0') { + if (agent_generation(a, NULL, 0, &result) == 0) { + agent_report_text(a, result.assistant); + msg_free(result.assistant); + } else if (a->err[0] != '\0') { if (a->report.len > 0) buf_addstr(&a->report, "\n\n"); buf_addf(&a->report, "subagent conclusion failed: %s", a->err); @@ -821,6 +780,7 @@ agent_run(struct worker *w, struct tool_ctx *tools, st { struct agent_state a; const struct tool_def *defs; + struct generation_result result; struct msg *m; int ndefs, round; @@ -829,7 +789,6 @@ agent_run(struct worker *w, struct tool_ctx *tools, st a.tools = tools; a.boot = boot; TAILQ_INIT(&a.conv); - buf_init(&a.text); buf_init(&a.report); a.data_total = boot->prompt.len; m = msg_new(ROLE_USER); @@ -839,34 +798,34 @@ agent_run(struct worker *w, struct tool_ctx *tools, st agent_progress(&a, "started"); for (round = 1; round <= AGENT_ITER_MAX; round++) { - if (agent_generation(&a, defs, ndefs) == -1) { + if (agent_generation(&a, defs, ndefs, &result) == -1) { agent_report_tail(&a); if (a.report.len > 0) buf_addstr(&a.report, "\n\n"); buf_addf(&a.report, "subagent provider error: %s", a.err); agent_finish(&a, 1); } - m = agent_assistant_message(&a); + m = result.assistant; TAILQ_INSERT_TAIL(&a.conv, m, entry); - if (agent_stop_limited(&a)) { - agent_report_text(&a); - m = agent_answer_dangling(&a); + if (result.outcome == GENERATION_LENGTH) { + agent_report_text(&a, m); + m = agent_answer_dangling(&a, m); TAILQ_INSERT_TAIL(&a.conv, m, entry); agent_conclude(&a, m); } - if (a.ncalls == 0) { + if (result.tool_calls == 0) { /* A normal completion returns only its final assistant text; * interim prose is retained solely as a cap fallback. */ buf_reset(&a.report); - agent_report_text(&a); + agent_report_text(&a, m); agent_finish(&a, 0); } /* Preserve tool-bearing prose so an iteration cap can still return * everything useful emitted before its one conclusion attempt. */ - agent_report_text(&a); + agent_report_text(&a, m); a.iterations++; - m = agent_execute_calls(&a); + m = agent_execute_calls(&a, m); TAILQ_INSERT_TAIL(&a.conv, m, entry); if (a.data_exceeded) { agent_report_tail(&a);