commit - 91e58933b7fe2e7ffb5f1943b2bdada616824b69
commit + 11cb7bb8d4c70c4973e2647be12cfd08cadbc8df
blob - 108c066fffece98abe222353b3520c7fbb28d169
blob + 401a79bf7f548c9d585d90ad873ad32d7b0357fe
--- regress/turn/agent.sh
+++ regress/turn/agent.sh
[ "$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
* 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
"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"
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) {
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
#include "log.h"
#include "xmalloc.h"
#include "buf.h"
-#include "json.h"
+#include "generation.h"
#include "msg.h"
#include "anthropic.h"
#include "openai.h"
#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;
}
}
-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)
{
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);
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);
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);
}
}
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;
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;
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)
{
}
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
"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);
{
struct agent_state a;
const struct tool_def *defs;
+ struct generation_result result;
struct msg *m;
int ndefs, round;
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);
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);