Skip to main content

noxid_codegen_server_js/
agents.rs

1//! WO-31 stages (b) and (c): the agent engine's server runtime section.
2//!
3//! Everything the loop needs is a compile-time constant emitted above
4//! `AGENT_RUNTIME`: the model binding, the embedded instructions, the turn
5//! ceiling, the derived tool registry, the two generated capability names, and
6//! the declared event algebra. The registry is the *only* dispatch table: an
7//! endpoint the compiler left out of it is not described to the model, is not
8//! reachable through the loop, and is not listed anywhere the model can see.
9//!
10//! The section is emitted only when the build declares an agent with `model:`,
11//! so a project with no engine agent carries none of this code.
12
13use noxid_agent_ir::{AgentDefinition, AgentEvent};
14use noxid_source::js_escape;
15
16/// The declared agents whose engine the emitted graph runs. Empty means the
17/// server graph carries no `agents` section at all.
18#[derive(Clone, Debug, Default)]
19pub struct AgentRuntimeOptions {
20    pub agents: Vec<AgentDefinition>,
21}
22
23impl AgentRuntimeOptions {
24    /// True when no declared agent carries an engine. A client-only agent
25    /// (ADR 0032, no `model:`) is not an engine agent and does not bring the
26    /// section into the graph.
27    pub fn is_empty(&self) -> bool {
28        !self.agents.iter().any(|agent| agent.engine.is_some())
29    }
30}
31
32fn event_javascript(event: &AgentEvent) -> String {
33    let payload_type = event
34        .payload_type
35        .as_ref()
36        .map(|ty| format!("\"{}\"", js_escape(&ty.to_string())))
37        .unwrap_or_else(|| "null".into());
38    // Two candidate validator ids: the boundary id the compiler minted for the
39    // payload type, then the declared-type alias every build emits. The engine
40    // takes the first that resolves and refuses the run if neither does.
41    let validators = event
42        .payload_type_id
43        .as_ref()
44        .map(|id| format!("\"{}\"", js_escape(id.as_str())))
45        .into_iter()
46        .chain(event.payload_type.as_ref().and_then(|ty| match ty {
47            noxid_types::Type::Named(name) => Some(format!("\"type:{}\"", js_escape(name))),
48            _ => None,
49        }))
50        .collect::<Vec<_>>()
51        .join(", ");
52    format!(
53        "Object.freeze({{ name: \"{}\", role: \"{}\", external: {}, payloadType: {payload_type}, validators: Object.freeze([{validators}]) }})",
54        js_escape(&event.name),
55        event.role.as_str(),
56        event.external,
57    )
58}
59
60fn contract_javascript(contract: &noxid_agent_ir::AgentContract) -> String {
61    let validators = contract
62        .type_id
63        .as_ref()
64        .map(|id| format!("\"{}\"", js_escape(id.as_str())))
65        .into_iter()
66        .chain(match &contract.ty {
67            noxid_types::Type::Named(name) => Some(format!("\"type:{}\"", js_escape(name))),
68            _ => None,
69        })
70        .collect::<Vec<_>>()
71        .join(", ");
72    format!(
73        "Object.freeze({{ id: \"{}\", type: \"{}\", validators: Object.freeze([{validators}]) }})",
74        js_escape(contract.id.as_str()),
75        js_escape(&contract.ty.to_string()),
76    )
77}
78
79/// Emit the `agents` runtime section, or the empty string when the build
80/// declares no engine agent.
81pub(crate) fn agents_runtime_javascript(
82    options: &AgentRuntimeOptions,
83    base_path: &str,
84    timeout_ms: u64,
85) -> String {
86    if options.is_empty() {
87        return String::new();
88    }
89    let prefix = if base_path == "/" {
90        "/_noxid/agents/".to_string()
91    } else {
92        format!("{}/_noxid/agents/", base_path.trim_end_matches('/'))
93    };
94    let declarations = options
95        .agents
96        .iter()
97        .filter_map(|agent| {
98            let engine = agent.engine.as_ref()?;
99            let events = agent
100                .events
101                .iter()
102                .map(event_javascript)
103                .collect::<Vec<_>>()
104                .join(", ");
105            let tools = engine
106                .tools
107                .iter()
108                .map(|tool| {
109                    let capabilities = tool
110                        .capabilities
111                        .iter()
112                        .map(|capability| format!("\"{}\"", js_escape(capability)))
113                        .collect::<Vec<_>>()
114                        .join(", ");
115                    format!(
116                        "Object.freeze({{ endpoint: \"{}\", version: {}, capabilities: Object.freeze([{capabilities}]), schemaHash: \"{}\" }})",
117                        js_escape(&tool.endpoint),
118                        tool.version,
119                        js_escape(&tool.schema_hash),
120                    )
121                })
122                .collect::<Vec<_>>()
123                .join(", ");
124            Some(format!(
125                "  \"{}\": Object.freeze({{ id: \"{}\", name: \"{}\", agentId: \"{}\", model: \"{}\", modelId: \"{}\", instructions: \"{}\", maxTurns: {}, timeoutMs: {timeout_ms}, runCapability: \"{}\", resumeCapability: \"{}\", input: {}, output: {}, events: Object.freeze([{events}]), tools: Object.freeze([{tools}]) }}),",
126                js_escape(&agent.name),
127                js_escape(agent.id.as_str()),
128                js_escape(&agent.name),
129                js_escape(&engine.agent_id),
130                js_escape(&engine.model),
131                js_escape(engine.model_id.as_str()),
132                js_escape(&engine.instructions.text),
133                engine.max_turns,
134                js_escape(&engine.run_capability),
135                js_escape(&engine.resume_capability),
136                contract_javascript(&agent.input),
137                contract_javascript(&agent.output),
138            ))
139        })
140        .collect::<Vec<_>>()
141        .join("\n");
142    // The event vocabulary is the IR's, rendered into the runtime rather than
143    // repeated in it: the build-time refusal and the runtime refusal read the
144    // same list.
145    let runtime = AGENT_RUNTIME.replace(
146        "__AGENT_EVENT_VOCABULARY__",
147        &format!(
148            "[{}]",
149            noxid_ir::AGENT_EVENT_VOCABULARY
150                .iter()
151                .map(|field| format!("\"{field}\""))
152                .collect::<Vec<_>>()
153                .join(", ")
154        ),
155    );
156    format!(
157        "\n// noxid-runtime:feature-start:agents\nconst agentRunPrefix = \"{}\";\nconst agentDeclarations = Object.freeze({{\n{declarations}\n}});\n{runtime}// noxid-runtime:feature-end:agents\n",
158        js_escape(&prefix),
159    )
160}
161
162pub(crate) const AGENT_RUNTIME: &str = r##"
163const AGENT_RUN_RECORD_SCHEMA = "noxid.agent.run.v1";
164const AGENT_RUN_ID = /^[A-Za-z0-9_-]{16,128}$/;
165const AGENT_RUN_TTL_SECONDS = 604_800;
166const AGENT_RUN_STATES = new Set(["Running", "Paused", "Completed", "Failed", "Cancelled"]);
167const AGENT_FINAL_ANSWER_TOOL = "noxid_final_answer";
168const AGENT_TOOL_RESULT_MAX_BYTES = 65_536;
169// The compiler-driven events (`ToolStarted`, `ToolCompleted`,
170// `PermissionRequired`) carry a developer-declared payload type, so the engine
171// projects a canonical record onto whatever fields that type declares. A
172// declared field outside this vocabulary is one the engine has no value for,
173// and the run refuses rather than inventing one.
174const AGENT_EVENT_VOCABULARY = Object.freeze(__AGENT_EVENT_VOCABULARY__);
175const agentRunStorage = __noxidStorage("agent_runs");
176const agentRunControllers = new Map();
177let agentReconciliation = null;
178
179function agentEngineError(code, message) {
180  return Object.assign(new Error(message), { code, agentEngine: true });
181}
182
183function agentDeclarationFor(name) {
184  return Object.hasOwn(agentDeclarations, name) ? agentDeclarations[name] : null;
185}
186
187function agentValidatorFor(contract, label, declaration) {
188  for (const key of contract.validators) {
189    const validator = typeValidators[key];
190    if (typeof validator === "function") return validator;
191  }
192  throw agentEngineError(
193    "AGENT_VALIDATOR_MISSING",
194    `agent \`${declaration.name}\` has no boundary validator for its ${label} type \`${contract.type}\`; the engine never accepts a value it cannot validate`,
195  );
196}
197
198function agentEventDefinition(declaration, name) {
199  return declaration.events.find((event) => event.name === name) ?? null;
200}
201
202function agentRunId() {
203  const value = globalThis.crypto?.randomUUID?.();
204  if (typeof value === "string" && AGENT_RUN_ID.test(value)) return value;
205  throw agentEngineError("AGENT_RUN_ID_UNAVAILABLE", "agent runs require crypto.randomUUID");
206}
207
208// ---------------------------------------------------------------------------
209// The derived registry, as the provider request sees it.
210// ---------------------------------------------------------------------------
211
212function agentToolEndpointSchema(tool) {
213  return endpointSchemas.find((schema) => schema.name === tool.endpoint && schema.version === tool.version) ?? null;
214}
215
216function agentJsonSchemaForType(type) {
217  if (type.startsWith("Optional<") && type.endsWith(">")) {
218    const inner = agentJsonSchemaForType(type.slice(9, -1));
219    return { anyOf: [inner, { type: "null" }] };
220  }
221  if (type.startsWith("Array<") && type.endsWith(">")) return { type: "array", items: agentJsonSchemaForType(type.slice(6, -1)) };
222  if (type === "String") return { type: "string" };
223  if (type === "Date") return { type: "string" };
224  if (type === "Boolean") return { type: "boolean" };
225  if (type === "Int") return { type: "integer" };
226  if (type === "Number" || type === "Float") return { type: "number" };
227  const declared = Object.hasOwn(modelTypeSchemas, `type:${type}`) ? modelTypeSchemas[`type:${type}`] : null;
228  if (declared !== null) return declared.schema;
229  return { type: "object" };
230}
231
232function agentToolInputSchema(schema) {
233  const properties = Object.create(null);
234  const required = [];
235  for (const field of [...schema.params, ...schema.query, ...schema.body]) {
236    properties[field.name] = agentJsonSchemaForType(field.type);
237    if (!field.type.startsWith("Optional<")) required.push(field.name);
238  }
239  return { type: "object", properties, required, additionalProperties: false };
240}
241
242function agentFinalAnswerSchema(declaration) {
243  const type = declaration.output.type;
244  const schema = agentJsonSchemaForType(type);
245  return schema.type === "object" || schema.properties !== undefined
246    ? schema
247    : { type: "object", properties: { value: schema }, required: ["value"], additionalProperties: false };
248}
249
250function agentFinalAnswerWrapped(declaration) {
251  const schema = agentFinalAnswerSchema(declaration);
252  return schema.properties !== undefined && Object.keys(schema.properties).length === 1 && Object.hasOwn(schema.properties, "value")
253    && !Object.hasOwn(modelTypeSchemas, `type:${declaration.output.type}`);
254}
255
256function agentToolRegistry(declaration) {
257  const entries = new Map();
258  for (const tool of declaration.tools) {
259    const schema = agentToolEndpointSchema(tool);
260    if (schema === null) {
261      throw agentEngineError(
262        "AGENT_TOOL_ENDPOINT_MISSING",
263        `agent \`${declaration.name}\` lists tool \`${tool.endpoint}@${tool.version}\`, which this build emits no endpoint for`,
264      );
265    }
266    entries.set(schema.name, Object.freeze({ tool, schema, capabilities: tool.capabilities }));
267  }
268  return entries;
269}
270
271function agentProviderTools(declaration, registry) {
272  const tools = [...registry.values()].map((entry) => Object.freeze({
273    name: entry.schema.name,
274    description: entry.schema.description ?? `Call the ${entry.schema.name} endpoint (${entry.schema.method} ${entry.schema.path}).`,
275    schema: agentToolInputSchema(entry.schema),
276  }));
277  tools.push(Object.freeze({
278    name: AGENT_FINAL_ANSWER_TOOL,
279    description: `Return the run's final ${declaration.output.type} answer and end the run.`,
280    schema: agentFinalAnswerSchema(declaration),
281  }));
282  return Object.freeze(tools);
283}
284
285// ---------------------------------------------------------------------------
286// The persisted run record. It is read back through the same validator the
287// engine wrote it with: a drifted record fails the resume rather than feeding
288// the loop a shape it never produced.
289// ---------------------------------------------------------------------------
290
291function agentRunKey(agent, runId) {
292  return `${agent}:${runId}`;
293}
294
295function agentValidTurn(turn) {
296  if (turn === null || typeof turn !== "object" || Array.isArray(turn)) return false;
297  if (!Number.isSafeInteger(turn.index) || turn.index < 0) return false;
298  if (typeof turn.text !== "string") return false;
299  if (!Array.isArray(turn.toolCalls) || !Array.isArray(turn.results)) return false;
300  for (const call of turn.toolCalls) {
301    if (call === null || typeof call !== "object") return false;
302    if (typeof call.id !== "string" || typeof call.name !== "string") return false;
303    if (call.arguments === null || typeof call.arguments !== "object") return false;
304  }
305  for (const result of turn.results) {
306    if (result === null || typeof result !== "object") return false;
307    if (typeof result.id !== "string" || typeof result.name !== "string") return false;
308    if (typeof result.ok !== "boolean") return false;
309  }
310  return true;
311}
312
313function agentValidRunRecord(record, agent) {
314  if (record === null || typeof record !== "object" || Array.isArray(record)) return null;
315  if (record.schema !== AGENT_RUN_RECORD_SCHEMA) return null;
316  if (typeof record.runId !== "string" || !AGENT_RUN_ID.test(record.runId)) return null;
317  if (record.agent !== agent) return null;
318  if (!AGENT_RUN_STATES.has(record.state)) return null;
319  if (record.principal !== null && typeof record.principal !== "string") return null;
320  if (!Number.isSafeInteger(record.version) || record.version < 1) return null;
321  if (!Array.isArray(record.turns) || !record.turns.every(agentValidTurn)) return null;
322  if (record.pending !== null && (typeof record.pending !== "object" || typeof record.pending?.name !== "string")) return null;
323  return record;
324}
325
326async function agentPersistRun(run) {
327  // Every write moves the version. A resume's compare-and-swap names the
328  // version it read, so a record that moved under it is a record it no
329  // longer owns.
330  run.version = Number.isSafeInteger(run.version) ? run.version + 1 : 1;
331  run.updatedAt = new Date().toISOString();
332  await agentRunStorage.set(agentRunKey(run.agent, run.runId), run, { ttl: AGENT_RUN_TTL_SECONDS });
333}
334
335// Acquiring a paused run is a decision, not a write. The compare-and-swap
336// names the state *and* the version the caller read, so of two concurrent
337// resumes exactly one moves the record to `Running` and dispatches the pending
338// tool; the loser never reaches the endpoint path, the provider, or the
339// event stream. A store that cannot swap conditionally refuses the resume
340// rather than dispatching a paused tool twice.
341async function agentAcquirePausedRun(run) {
342  if (typeof agentRunStorage.compareAndSet !== "function") {
343    throw agentEngineError("AGENT_RUN_STORE_UNAVAILABLE", "the agent run store cannot claim a paused run exclusively, so the resume is refused rather than risking a second dispatch of the same tool");
344  }
345  const next = {
346    ...run,
347    state: "Running",
348    approved: run.pending.id,
349    pending: null,
350    version: run.version + 1,
351    updatedAt: new Date().toISOString(),
352  };
353  const acquired = await agentRunStorage.compareAndSet(
354    agentRunKey(run.agent, run.runId),
355    { state: "Paused", version: run.version },
356    next,
357    { ttl: AGENT_RUN_TTL_SECONDS },
358  );
359  return acquired ? next : null;
360}
361
362async function agentLoadRun(agent, runId) {
363  const stored = await agentRunStorage.get(agentRunKey(agent, runId));
364  if (stored === null) return null;
365  const valid = agentValidRunRecord(stored, agent);
366  if (valid === null) {
367    await agentRunStorage.delete(agentRunKey(agent, runId));
368    throw agentEngineError("AGENT_RUN_RECORD_DRIFT", `agent \`${agent}\` run \`${runId}\` is persisted in a shape this build did not write; it was discarded rather than resumed`);
369  }
370  return valid;
371}
372
373/// Startup reconciliation, owned by the WO-24 queue worker. A pause is durable,
374/// so a paused run survives the process that created it; a run still marked
375/// `Running` when a process starts was orphaned by the previous process's exit
376/// and is failed with `AGENT_TIMEOUT` rather than left waiting forever.
377async function __noxidReconcileAgentRuns() {
378  if (agentReconciliation !== null) return agentReconciliation;
379  agentReconciliation = (async () => {
380    let paused = 0;
381    let orphaned = 0;
382    let keys;
383    try { keys = await agentRunStorage.list(""); }
384    catch { agentReconciliation = null; throw agentEngineError("AGENT_RUN_STORE_UNAVAILABLE", "agent run reconciliation cannot read the agent_runs namespace"); }
385    for (const key of keys) {
386      const separator = key.indexOf(":");
387      if (separator <= 0) continue;
388      const agent = key.slice(0, separator);
389      const declaration = agentDeclarationFor(agent);
390      if (declaration === null) continue;
391      let record;
392      try { record = await agentLoadRun(agent, key.slice(separator + 1)); }
393      catch { continue; }
394      if (record === null) continue;
395      if (record.state === "Paused") { paused += 1; continue; }
396      if (record.state !== "Running") continue;
397      record.state = "Failed";
398      record.error = { code: "AGENT_TIMEOUT", message: "The run was interrupted by a process restart and exceeded its declared timeout" };
399      await agentPersistRun(record);
400      orphaned += 1;
401    }
402    return Object.freeze({ paused, orphaned });
403  })();
404  return agentReconciliation;
405}
406
407// ---------------------------------------------------------------------------
408// Events.
409// ---------------------------------------------------------------------------
410
411function agentProjectPayload(declaration, eventName, typeName, canonical) {
412  const entry = Object.hasOwn(modelTypeSchemas, `type:${typeName}`) ? modelTypeSchemas[`type:${typeName}`] : null;
413  const properties = entry?.schema?.properties ?? null;
414  if (properties === null) {
415    throw agentEngineError(
416      "AGENT_EVENT_UNREPRESENTABLE",
417      `agent \`${declaration.name}\` declares \`event ${eventName}(${typeName})\`, and \`${typeName}\` is not a declared record type the engine can fill; declare it as a type whose fields are among ${AGENT_EVENT_VOCABULARY.join(", ")}`,
418    );
419  }
420  const payload = Object.create(null);
421  for (const key of Object.keys(properties)) {
422    if (!Object.hasOwn(canonical, key)) {
423      throw agentEngineError(
424        "AGENT_EVENT_UNREPRESENTABLE",
425        `agent \`${declaration.name}\` declares \`event ${eventName}(${typeName})\` with field \`${key}\`, which the engine has no value for; a compiler-driven ${eventName} payload may declare ${AGENT_EVENT_VOCABULARY.join(", ")}`,
426      );
427    }
428    payload[key] = canonical[key];
429  }
430  return payload;
431}
432
433function agentEvent(declaration, name, canonical = null, raw = undefined) {
434  const definition = agentEventDefinition(declaration, name);
435  if (definition === null) return null;
436  if (definition.payloadType === null) return Object.freeze({ tag: name });
437  const value = canonical === null
438    ? raw
439    : (definition.payloadType === "String" ? canonical.summary ?? canonical.name : agentProjectPayload(declaration, name, definition.payloadType, canonical));
440  // Every value that reaches here is compiler-constructed (`Failed`,
441  // `Paused`, `Token`), already validated (`Completed`, through the output
442  // validator), or projected field-by-field onto a declared type from the
443  // canonical record. When the build emits a validator for the payload type it
444  // runs; `AgentError` and the compiler-owned `Paused` payload have no declared
445  // type and therefore no validator to run.
446  let validator = null;
447  for (const key of definition.validators) {
448    if (typeof typeValidators[key] === "function") { validator = typeValidators[key]; break; }
449  }
450  const trusted = validator === null ? value : validator(value, true);
451  return Object.freeze({ tag: name, value: trusted === undefined ? null : trusted });
452}
453
454// ---------------------------------------------------------------------------
455// The provider turn. One streaming call per turn, with the registry's tool
456// schemas attached; text deltas surface as `Token`, tool-use blocks accumulate
457// into the turn's calls.
458// ---------------------------------------------------------------------------
459
460function agentAnthropicMessages(declaration, run) {
461  const messages = [{ role: "user", content: JSON.stringify(run.input) }];
462  for (const turn of run.turns) {
463    const content = [];
464    if (turn.text.length !== 0) content.push({ type: "text", text: turn.text });
465    for (const call of turn.toolCalls) content.push({ type: "tool_use", id: call.id, name: call.name, input: call.arguments });
466    if (content.length !== 0) messages.push({ role: "assistant", content });
467    if (turn.results.length !== 0) {
468      messages.push({
469        role: "user",
470        content: turn.results.map((result) => ({ type: "tool_result", tool_use_id: result.id, content: JSON.stringify(result.content), is_error: result.ok === false })),
471      });
472    }
473  }
474  return messages;
475}
476
477function agentOpenAiMessages(declaration, run) {
478  const messages = [{ role: "system", content: declaration.instructions }, { role: "user", content: JSON.stringify(run.input) }];
479  for (const turn of run.turns) {
480    const message = { role: "assistant", content: turn.text.length === 0 ? null : turn.text };
481    if (turn.toolCalls.length !== 0) {
482      message.tool_calls = turn.toolCalls.map((call) => ({ id: call.id, type: "function", function: { name: call.name, arguments: JSON.stringify(call.arguments) } }));
483    }
484    messages.push(message);
485    for (const result of turn.results) messages.push({ role: "tool", tool_call_id: result.id, content: JSON.stringify(result.content) });
486  }
487  return messages;
488}
489
490function agentParsedArguments(raw) {
491  if (raw.length === 0) return Object.create(null);
492  try {
493    const value = JSON.parse(raw);
494    return value !== null && typeof value === "object" && !Array.isArray(value) ? value : Object.create(null);
495  } catch { return Object.create(null); }
496}
497
498async function* agentAnthropicTurn(declaration, definition, run, tools, signal) {
499  const body = {
500    model: definition.modelId,
501    max_tokens: definition.maxTokens === null ? 1024 : definition.maxTokens,
502    system: declaration.instructions,
503    messages: agentAnthropicMessages(declaration, run),
504    tools: tools.map((tool) => ({ name: tool.name, description: tool.description, input_schema: tool.schema })),
505    stream: true,
506  };
507  if (definition.temperature !== null) body.temperature = definition.temperature;
508  const response = await modelSend(definition, "/v1/messages", anthropicHeaders(definition), body, signal, true);
509  let text = "";
510  const blocks = new Map();
511  let input = 0;
512  let output = 0;
513  for await (const event of modelSseEvents(definition, response, signal)) {
514    if (event?.type === "message_start") input = event?.message?.usage?.input_tokens ?? input;
515    if (event?.type === "content_block_start" && event?.content_block?.type === "tool_use") {
516      blocks.set(event.index, { id: event.content_block.id, name: event.content_block.name, raw: "" });
517    }
518    if (event?.type === "content_block_delta" && typeof event?.delta?.text === "string") {
519      text += event.delta.text;
520      yield event.delta.text;
521    }
522    if (event?.type === "content_block_delta" && typeof event?.delta?.partial_json === "string") {
523      const block = blocks.get(event.index);
524      if (block !== undefined) block.raw += event.delta.partial_json;
525    }
526    if (event?.type === "message_delta") output = event?.usage?.output_tokens ?? output;
527  }
528  return Object.freeze({
529    text,
530    toolCalls: [...blocks.values()].map((block) => Object.freeze({ id: block.id, name: block.name, arguments: agentParsedArguments(block.raw) })),
531    usage: modelUsage(input, output),
532  });
533}
534
535async function* agentOpenAiTurn(declaration, definition, run, tools, signal) {
536  const body = {
537    model: definition.modelId,
538    messages: agentOpenAiMessages(declaration, run),
539    max_tokens: definition.maxTokens === null ? 1024 : definition.maxTokens,
540    tools: tools.map((tool) => ({ type: "function", function: { name: tool.name, description: tool.description, parameters: tool.schema } })),
541    stream: true,
542    stream_options: { include_usage: true },
543  };
544  if (definition.temperature !== null) body.temperature = definition.temperature;
545  const response = await modelSend(definition, "/v1/chat/completions", openaiHeaders(definition), body, signal, true);
546  let text = "";
547  const calls = new Map();
548  let input = 0;
549  let output = 0;
550  for await (const event of modelSseEvents(definition, response, signal)) {
551    const delta = event?.choices?.[0]?.delta;
552    if (typeof delta?.content === "string" && delta.content.length !== 0) {
553      text += delta.content;
554      yield delta.content;
555    }
556    if (Array.isArray(delta?.tool_calls)) {
557      for (const call of delta.tool_calls) {
558        const index = Number.isSafeInteger(call?.index) ? call.index : 0;
559        const existing = calls.get(index) ?? { id: "", name: "", raw: "" };
560        if (typeof call?.id === "string" && call.id.length !== 0) existing.id = call.id;
561        if (typeof call?.function?.name === "string" && call.function.name.length !== 0) existing.name = call.function.name;
562        if (typeof call?.function?.arguments === "string") existing.raw += call.function.arguments;
563        calls.set(index, existing);
564      }
565    }
566    if (event?.usage) {
567      input = event.usage.prompt_tokens ?? input;
568      output = event.usage.completion_tokens ?? output;
569    }
570  }
571  return Object.freeze({
572    text,
573    toolCalls: [...calls.values()].filter((call) => call.name.length !== 0).map((call, index) => Object.freeze({ id: call.id.length === 0 ? `call_${index}` : call.id, name: call.name, arguments: agentParsedArguments(call.raw) })),
574    usage: modelUsage(input, output),
575  });
576}
577
578// The scenario side of the loop. `noxid test` installs the WO-30 controller
579// with a per-agent turn script; while it is installed the engine performs no
580// provider I/O at all, and a turn the script does not supply fails closed with
581// `MODEL_STUB_REQUIRED` rather than reaching a provider. The tool-call shape a
582// scripted turn produces is exactly the shape the two provider readers
583// produce, so the loop below this point cannot tell the difference — which is
584// the point: a scenario exercises the real loop.
585async function* agentScenarioTurn(declaration, controller) {
586  const script = controller.takeAgentTurn(declaration.name);
587  if (script === null || script === undefined) {
588    throw modelError(
589      "MODEL_STUB_REQUIRED",
590      `scenario ran agent \`${declaration.name}\` past its scripted turns with no stub left; add another entry to \`given: agent ${declaration.name} = turns [ ... ]\` (\`text "..."\`, \`tool <Endpoint> { field = value }\`, or \`final { field = value }\`)`,
591    );
592  }
593  let text = "";
594  for (const chunk of script.text) { text += chunk; yield chunk; }
595  const toolCalls = [];
596  if (script.call !== null && script.call !== undefined) {
597    const isFinal = script.call.kind === "final";
598    const name = isFinal ? AGENT_FINAL_ANSWER_TOOL : script.call.endpoint;
599    const args = isFinal && agentFinalAnswerWrapped(declaration)
600      ? { value: script.call.arguments?.value }
601      : script.call.arguments;
602    toolCalls.push(Object.freeze({ id: `scenario_call_${script.index}`, name, arguments: args }));
603  }
604  return Object.freeze({
605    text,
606    toolCalls: Object.freeze(toolCalls),
607    usage: modelUsage(script.inputTokens ?? 0, script.outputTokens ?? 0),
608  });
609}
610
611function agentProviderTurn(declaration, definition, run, tools, signal) {
612  const controller = globalThis.__NOXID_MODEL_SCENARIO__;
613  if (controller !== undefined && controller !== null && typeof controller.takeAgentTurn === "function") {
614    return agentScenarioTurn(declaration, controller);
615  }
616  return definition.provider === "anthropic"
617    ? agentAnthropicTurn(declaration, definition, run, tools, signal)
618    : agentOpenAiTurn(declaration, definition, run, tools, signal);
619}
620
621// ---------------------------------------------------------------------------
622// Tool dispatch: the full endpoint path, under the run's principal.
623// ---------------------------------------------------------------------------
624
625// Three-valued, because a deferred capability is not a denial: the host
626// authorizer may answer `true`, `"defer"` / `{ defer: true }`, or anything
627// else, and only the first runs the endpoint.
628async function agentAuthorizeTool(declaration, entry, request, environment, executionContext, signal, route) {
629  if (entry.capabilities.length === 0) return Object.freeze({ kind: "allowed" });
630  if (typeof authorize !== "function") return Object.freeze({ kind: "denied", capability: entry.capabilities[0], reason: "no authorizer is configured" });
631  for (const capability of entry.capabilities) {
632    let decision;
633    try {
634      decision = await authorize(Object.freeze({
635        capability,
636        semanticId: entry.schema.id,
637        traceId: __noxidTraceIdForRequest(request),
638        target: "agent",
639        agent: declaration.name,
640        route,
641        request,
642        environment,
643        executionContext,
644        signal,
645      }));
646    } catch { decision = false; }
647    if (decision === true) continue;
648    if (decision === "defer" || decision?.defer === true) return Object.freeze({ kind: "deferred", capability });
649    return Object.freeze({ kind: "denied", capability, reason: "the host authorizer denied it" });
650  }
651  return Object.freeze({ kind: "allowed" });
652}
653
654async function agentToolResponseBody(response) {
655  const text = await mcpBoundedResponseText(response);
656  if (text.length > AGENT_TOOL_RESULT_MAX_BYTES) {
657    throw agentEngineError("AGENT_TOOL_RESULT_TOO_LARGE", "the tool result exceeds the bounded agent tool-result size");
658  }
659  if (text.length === 0) return null;
660  try { return JSON.parse(text); } catch { return { text }; }
661}
662
663async function agentCallTool(declaration, entry, call, outerRequest, environment, executionContext) {
664  let endpointRequest;
665  try { endpointRequest = mcpEndpointRequest(outerRequest, entry.schema, call.arguments); }
666  catch (cause) {
667    return Object.freeze({ ok: false, content: Object.freeze({ code: "AGENT_TOOL_ARGUMENTS_INVALID", message: cause?.message ?? "the tool arguments could not be encoded for the endpoint" }) });
668  }
669  inheritNoxidRequestTrace(outerRequest, endpointRequest);
670  // The run's principal, erased to the runtime shape the kernel already
671  // builds: `agent:<AgentId>:acting:<session|system>`.
672  __noxidAgentRequests.set(endpointRequest, declaration.agentId);
673  const response = await handleEndpointRequest(endpointRequest, new URL(endpointRequest.url), environment, executionContext);
674  if (response === null) {
675    return Object.freeze({ ok: false, content: Object.freeze({ code: "AGENT_TOOL_DISPATCH_FAILED", message: `tool ${entry.schema.name} did not resolve to its declared endpoint` }) });
676  }
677  __noxidTraceResponseFailure(endpointRequest, response);
678  // Scenario observation only, and only of what the endpoint boundary already
679  // saw: the arguments the run dispatched and the status the endpoint's own
680  // validator produced. Nothing here changes the dispatch.
681  {
682    const observer = globalThis.__NOXID_MODEL_SCENARIO__;
683    if (observer !== undefined && observer !== null && typeof observer.recordAgentToolCall === "function") {
684      observer.recordAgentToolCall({ agent: declaration.name, tool: entry.schema.name, arguments: call.arguments, status: response.status, ok: response.ok });
685    }
686  }
687  let body;
688  try { body = await agentToolResponseBody(response); }
689  catch (cause) {
690    return Object.freeze({ ok: false, content: Object.freeze({ code: cause?.code ?? "AGENT_TOOL_RESULT_FAILED", message: cause?.message ?? "the tool result could not be represented safely" }) });
691  }
692  // The endpoint has already validated its own result against its declared
693  // type; an `ok: false` body is a refusal the model is told about verbatim,
694  // not a run failure.
695  if (!response.ok || body?.ok === false) {
696    return Object.freeze({ ok: false, content: Object.freeze({ status: response.status, error: body?.error ?? null }) });
697  }
698  return Object.freeze({ ok: true, content: body?.ok === true ? body.value ?? null : body });
699}
700
701// ---------------------------------------------------------------------------
702// The loop.
703// ---------------------------------------------------------------------------
704
705async function* agentRunLoop(declaration, run, context) {
706  const definition = modelDefinitionFor(declaration.model);
707  const registry = agentToolRegistry(declaration);
708  const tools = agentProviderTools(declaration, registry);
709  const outputValidator = agentValidatorFor(declaration.output, "output", declaration);
710  const wrapped = agentFinalAnswerWrapped(declaration);
711  const runStarted = Date.now();
712  const runTrace = __noxidTraceForRequest(context.request) ?? (tracingMode === "full" ? __noxidTraceContext() : null);
713  let tokensInput = 0;
714  let tokensOutput = 0;
715
716  const finish = async (state, error, output) => {
717    run.state = state;
718    run.error = error;
719    run.output = output ?? null;
720    run.pending = null;
721    await agentPersistRun(run);
722    __noxidTraceEmit(runTrace, "agent.run", {
723      semanticId: declaration.id,
724      agent: declaration.name,
725      agentRun: run.runId,
726      state,
727      durationMs: Date.now() - runStarted,
728      tokensInput,
729      tokensOutput,
730      code: error?.code,
731    });
732  };
733
734  // One abort, two meanings: the WO-18 deadline fails the run with
735  // `AGENT_TIMEOUT`, a client disconnect or an explicit cancel ends it as
736  // `Cancelled`. Both stop the model call and the run.
737  async function* stopped() {
738    if (context.abortKind() === "timeout") {
739      const error = { code: "AGENT_TIMEOUT", message: `agent \`${declaration.name}\` exceeded its declared ${declaration.timeoutMs} ms run timeout` };
740      await finish("Failed", error, null);
741      yield agentEvent(declaration, "Failed", null, error);
742      return;
743    }
744    await finish("Cancelled", null, null);
745    const cancelled = agentEvent(declaration, "Cancelled");
746    if (cancelled !== null) yield cancelled;
747  }
748
749  const started = agentEvent(declaration, "Started");
750  if (started !== null) yield started;
751
752  while (true) {
753    if (context.signal.aborted) { yield* stopped(); return; }
754
755    // A resumed run finishes the turn the pause froze before it asks the model
756    // for another one: the approved call is the first work it does.
757    const last = run.turns[run.turns.length - 1] ?? null;
758    const resuming = last !== null && last.results.length < last.toolCalls.length;
759    let turn;
760    if (resuming) {
761      turn = last;
762    } else {
763      if (run.turns.length >= declaration.maxTurns) {
764        const error = { code: "AGENT_MAX_TURNS", message: `agent \`${declaration.name}\` reached its declared ceiling of ${declaration.maxTurns} turns without a final answer` };
765        await finish("Failed", error, null);
766        yield agentEvent(declaration, "Failed", null, error);
767        return;
768      }
769      const turnIndex = run.turns.length;
770      const turnStarted = Date.now();
771      let assistant;
772      try {
773        const source = agentProviderTurn(declaration, definition, run, tools, context.signal);
774        for (;;) {
775          const step = await source.next();
776          if (step.done) { assistant = step.value; break; }
777          const token = agentEvent(declaration, "Token", null, step.value);
778          if (token !== null) yield token;
779        }
780      } catch (cause) {
781        if (context.signal.aborted) { yield* stopped(); return; }
782        const error = {
783          code: "AGENT_MODEL_FAILED",
784          message: `agent \`${declaration.name}\` could not complete a model turn (${cause?.code ?? "MODEL_PROVIDER_ERROR"})`,
785        };
786        await finish("Failed", error, null);
787        yield agentEvent(declaration, "Failed", null, error);
788        return;
789      }
790      tokensInput += assistant.usage.inputTokens;
791      tokensOutput += assistant.usage.outputTokens;
792      __noxidTraceEmit(runTrace, "agent.turn", {
793        semanticId: declaration.id,
794        agent: declaration.name,
795        agentRun: run.runId,
796        agentTurn: turnIndex,
797        durationMs: Date.now() - turnStarted,
798        tokensInput: assistant.usage.inputTokens,
799        tokensOutput: assistant.usage.outputTokens,
800      });
801
802      turn = { index: turnIndex, text: assistant.text, toolCalls: assistant.toolCalls.map((call) => ({ id: call.id, name: call.name, arguments: call.arguments })), results: [] };
803      run.turns.push(turn);
804      await agentPersistRun(run);
805
806      const finalCall = turn.toolCalls.find((call) => call.name === AGENT_FINAL_ANSWER_TOOL) ?? null;
807      if (finalCall !== null || turn.toolCalls.length === 0) {
808        let candidate;
809        if (finalCall !== null) candidate = wrapped ? finalCall.arguments.value : finalCall.arguments;
810        else {
811          try { candidate = JSON.parse(turn.text); }
812          catch {
813            const error = { code: "AGENT_OUTPUT_INVALID", message: `agent \`${declaration.name}\` ended a turn without calling \`${AGENT_FINAL_ANSWER_TOOL}\`, and its text is not a \`${declaration.output.type}\` value` };
814            await finish("Failed", error, null);
815            yield agentEvent(declaration, "Failed", null, error);
816            return;
817          }
818        }
819        let output;
820        try { output = outputValidator(candidate, true); }
821        catch (cause) {
822          const error = { code: "AGENT_OUTPUT_INVALID", message: `agent \`${declaration.name}\` produced a final answer that violates its declared \`${declaration.output.type}\` output: ${cause?.message ?? String(cause)}` };
823          await finish("Failed", error, null);
824          yield agentEvent(declaration, "Failed", null, error);
825          return;
826        }
827        await finish("Completed", null, output === undefined ? null : output);
828        yield agentEvent(declaration, "Completed", null, output === undefined ? null : output);
829        return;
830      }
831    }
832
833    while (turn.results.length < turn.toolCalls.length) {
834      if (context.signal.aborted) { yield* stopped(); return; }
835      const call = turn.toolCalls[turn.results.length];
836      const entry = registry.get(call.name) ?? null;
837      if (entry === null) {
838        // A tool outside the derived registry is unreachable, and asking for it
839        // is not a failure: the model is told the name is not available and the
840        // loop continues with that as the tool result.
841        turn.results.push({ id: call.id, name: call.name, ok: false, content: { code: "AGENT_TOOL_NOT_AVAILABLE", message: `\`${call.name}\` is not one of this agent's tools; the available tools are ${[...registry.keys(), AGENT_FINAL_ANSWER_TOOL].join(", ")}` } });
842        await agentPersistRun(run);
843        continue;
844      }
845      const preapproved = run.approved === call.id;
846      const decision = preapproved
847        ? Object.freeze({ kind: "allowed" })
848        : await agentAuthorizeTool(declaration, entry, context.request, context.environment, context.executionContext, context.signal, context.route);
849      if (preapproved) {
850        delete run.approved;
851        await agentPersistRun(run);
852      }
853      if (decision.kind === "deferred") {
854        run.state = "Paused";
855        run.pending = { id: call.id, name: call.name, endpoint: entry.schema.name, capability: decision.capability, arguments: call.arguments, turn: turn.index };
856        await agentPersistRun(run);
857        const permission = agentEvent(declaration, "PermissionRequired", agentToolCanonical(declaration, run, entry, call, turn, {
858          status: "deferred", ok: false, code: "AGENT_PERMISSION_REQUIRED",
859          message: `capability ${decision.capability} was deferred to a human`,
860          summary: `awaiting approval for ${decision.capability}`,
861          capability: decision.capability,
862        }));
863        if (permission !== null) yield permission;
864        __noxidTraceEmit(runTrace, "agent.run", {
865          semanticId: declaration.id, agent: declaration.name, agentRun: run.runId,
866          state: "Paused", durationMs: Date.now() - runStarted, tokensInput, tokensOutput,
867        });
868        yield agentEvent(declaration, "Paused", null, run.runId);
869        return;
870      }
871      const toolStarted = agentEvent(declaration, "ToolStarted", agentToolCanonical(declaration, run, entry, call, turn, { status: "started", ok: true }));
872      if (toolStarted !== null) yield toolStarted;
873      const toolBegan = Date.now();
874      let outcome;
875      if (decision.kind === "denied") {
876        outcome = Object.freeze({ ok: false, content: Object.freeze({ code: "AGENT_TOOL_DENIED", message: `capability ${decision.capability} was refused: ${decision.reason}` }) });
877      } else {
878        try { outcome = await agentCallTool(declaration, entry, call, context.request, context.environment, context.executionContext); }
879        catch (cause) {
880          if (context.signal.aborted) { yield* stopped(); return; }
881          const error = { code: "AGENT_TOOL_FAILED", message: `agent \`${declaration.name}\` could not dispatch tool \`${entry.schema.name}\` (${cause?.code ?? "unknown"})` };
882          await finish("Failed", error, null);
883          yield agentEvent(declaration, "Failed", null, error);
884          return;
885        }
886      }
887      __noxidTraceEmit(runTrace, "agent.tool", {
888        semanticId: declaration.id, agent: declaration.name, agentRun: run.runId, agentTurn: turn.index,
889        toolEndpoint: entry.schema.id, durationMs: Date.now() - toolBegan,
890        code: outcome.ok ? undefined : "AGENT_TOOL_FAILED",
891      });
892      turn.results.push({ id: call.id, name: call.name, ok: outcome.ok, content: outcome.content });
893      await agentPersistRun(run);
894      const completed = agentEvent(declaration, "ToolCompleted", agentToolCanonical(declaration, run, entry, call, turn, {
895        status: outcome.ok ? "completed" : "failed",
896        ok: outcome.ok,
897        summary: outcome.ok ? `${entry.schema.name} completed` : `${entry.schema.name} failed`,
898        code: outcome.ok ? "" : outcome.content?.code ?? "AGENT_TOOL_FAILED",
899        message: outcome.ok ? "" : outcome.content?.message ?? "the tool refused",
900        result: JSON.stringify(outcome.content ?? null),
901      }));
902      if (completed !== null) yield completed;
903    }
904  }
905}
906
907function agentToolCanonical(declaration, run, entry, call, turn, overrides) {
908  return {
909    name: entry.schema.name,
910    tool: entry.schema.name,
911    endpoint: entry.schema.id,
912    capability: entry.capabilities[0] ?? "",
913    arguments: JSON.stringify(call.arguments),
914    summary: entry.schema.name,
915    status: "started",
916    ok: true,
917    code: "",
918    message: "",
919    runId: run.runId,
920    turn: turn.index,
921    agent: declaration.name,
922    result: null,
923    ...overrides,
924  };
925}
926
927// ---------------------------------------------------------------------------
928// The SSE session.
929// ---------------------------------------------------------------------------
930
931function agentEndpointSchema(declaration, path) {
932  return Object.freeze({
933    id: declaration.id,
934    name: declaration.name,
935    version: 1,
936    description: null,
937    kind: "stream",
938    method: "POST",
939    path,
940    params: Object.freeze([]),
941    query: Object.freeze([]),
942    body: Object.freeze([]),
943    result: Object.freeze({ id: declaration.id, type: "String", typeId: null, validator: "", errorValidator: null }),
944    capabilities: Object.freeze([]),
945    timeoutMs: declaration.timeoutMs,
946    limit: null,
947    cache: null,
948    idempotent: false,
949    middleware: Object.freeze([]),
950    invalidates: Object.freeze([]),
951  });
952}
953
954function agentStreamResponse(schema, run, events, headers, release) {
955  const encoder = new TextEncoder();
956  let sequence = 0;
957  let iterator = null;
958  const body = new ReadableStream({
959    async start(controller) {
960      const emit = (frame) => {
961        try { controller.enqueue(encoder.encode(frame)); return true; }
962        catch { return false; }
963      };
964      try {
965        iterator = events[Symbol.asyncIterator]();
966        for (;;) {
967          const next = await iterator.next();
968          if (next.done) break;
969          if (next.value === null || next.value === undefined) continue;
970          sequence += 1;
971          if (!emit(streamFrame("message", next.value, `${run.runId}:${sequence}`))) break;
972        }
973      } catch (cause) {
974        const code = typeof cause?.code === "string" ? cause.code : "AGENT_RUN_FAILED";
975        emit(streamErrorFrame(schema, code, cause?.agentEngine === true ? cause.message : "The agent run failed"));
976      } finally {
977        release();
978        try { controller.close(); } catch {}
979      }
980    },
981    async cancel() {
982      const controller = agentRunControllers.get(run.runId);
983      if (controller !== undefined) controller.abort("disconnect");
984      if (iterator !== null && typeof iterator.return === "function") {
985        try { await iterator.return(); } catch {}
986      }
987      release();
988    },
989  });
990  return endpointResponseWithHeaders(new Response(body, { status: 200, headers: endpointStreamHeaders() }), headers);
991}
992
993async function agentSession(request, declaration, schema, capability, environment, executionContext, prepare) {
994  return withEndpointDeadline(schema, async (deadlineSignal, deadlineAt) => {
995    const middleware = await applyEndpointMiddleware(request, schema, Object.create(null), Object.create(null), environment, executionContext, deadlineSignal);
996    if (middleware.response) return endpointResponseWithHeaders(middleware.response, middleware.headers);
997    const guarded = Object.freeze({ ...schema, capabilities: Object.freeze([capability]) });
998    const authorization = await authorizeEndpoint(request, guarded, middleware.route, environment, executionContext, deadlineSignal);
999    if (authorization) return endpointResponseWithHeaders(authorization, middleware.headers);
1000    const middlewareContext = middleware.context ?? EMPTY_MIDDLEWARE_CONTEXT;
1001    // `Principal.Agent { id: AgentId(<agent>), actingFor }`, erased to the
1002    // runtime shape the kernel already builds for an agent-attributed request.
1003    const principal = __noxidPrincipal(middlewareContext, environment, declaration.agentId);
1004    __noxidTraceBindPrincipal({ request }, principal);
1005    __noxidTraceSemantic(request, "endpoint", declaration.id);
1006    let prepared;
1007    try { prepared = await prepare(principal, middlewareContext, middleware, deadlineSignal); }
1008    catch (cause) {
1009      const code = typeof cause?.code === "string" ? cause.code : "AGENT_RUN_FAILED";
1010      return endpointResponseWithHeaders(failure(cause?.status ?? 500, code, cause?.agentEngine === true ? cause.message : "The agent run could not start", declaration.id), middleware.headers);
1011    }
1012    if (prepared.response) return endpointResponseWithHeaders(prepared.response, middleware.headers);
1013    const run = prepared.run;
1014    // `withEndpointDeadline` clears its own timer the moment this operation
1015    // resolves with the streaming response, so the run owns the remainder of
1016    // the declared timeout itself — exactly as a stream endpoint does.
1017    const controller = new AbortController();
1018    let abortKind = null;
1019    const abort = (kind) => {
1020      if (controller.signal.aborted) return;
1021      abortKind = kind;
1022      controller.abort(kind);
1023    };
1024    const onDisconnect = () => abort("disconnect");
1025    request.signal.addEventListener("abort", onDisconnect, { once: true });
1026    if (request.signal.aborted) abort("disconnect");
1027    const timer = setTimeout(() => abort("timeout"), Math.max(0, deadlineAt - Date.now()));
1028    agentRunControllers.set(run.runId, controller);
1029    const release = () => {
1030      clearTimeout(timer);
1031      request.signal.removeEventListener("abort", onDisconnect);
1032      if (agentRunControllers.get(run.runId) === controller) agentRunControllers.delete(run.runId);
1033    };
1034    const context = Object.freeze({
1035      request,
1036      environment,
1037      executionContext,
1038      signal: controller.signal,
1039      abortKind: () => abortKind,
1040      principal,
1041      route: middleware.route,
1042    });
1043    return agentStreamResponse(schema, run, agentRunLoop(declaration, run, context), middleware.headers, release);
1044  });
1045}
1046
1047async function agentDecodeInput(declaration, request) {
1048  let payload;
1049  try { payload = JSON.parse(await request.text()); }
1050  catch { throw Object.assign(agentEngineError("AGENT_INPUT_INVALID", "an agent run requires a JSON body of the form { \"input\": ... }"), { status: 400 }); }
1051  if (payload === null || typeof payload !== "object" || Array.isArray(payload) || !Object.hasOwn(payload, "input")) {
1052    throw Object.assign(agentEngineError("AGENT_INPUT_INVALID", "an agent run requires a JSON body of the form { \"input\": ... }"), { status: 400 });
1053  }
1054  const validator = agentValidatorFor(declaration.input, "input", declaration);
1055  try { return validator(payload.input, true); }
1056  catch (cause) {
1057    throw Object.assign(agentEngineError("AGENT_INPUT_INVALID", `the run input violates the declared \`${declaration.input.type}\` type: ${cause?.message ?? String(cause)}`), { status: 422 });
1058  }
1059}
1060
1061async function handleAgentRunRequest(request, url, environment, executionContext) {
1062  if (!url.pathname.startsWith(agentRunPrefix)) return null;
1063  let segments;
1064  try { segments = url.pathname.slice(agentRunPrefix.length).split("/").filter((segment) => segment.length !== 0).map(decodeURIComponent); }
1065  catch { return failure(400, "AGENT_PATH_ENCODING_INVALID", "The agent run path contains invalid percent encoding"); }
1066  if (segments.length !== 2 && segments.length !== 4) return failure(404, "AGENT_NOT_FOUND", "No agent run surface exists at this path");
1067  if (segments[1] !== "runs") return failure(404, "AGENT_NOT_FOUND", "No agent run surface exists at this path");
1068  if (segments.length === 4 && segments[3] !== "resume") return failure(404, "AGENT_NOT_FOUND", "No agent run surface exists at this path");
1069  const declaration = agentDeclarationFor(segments[0]);
1070  if (declaration === null) return failure(404, "AGENT_NOT_FOUND", `No engine agent named ${segments[0]} is declared`);
1071  if (request.method !== "POST") return failure(405, "AGENT_METHOD_NOT_ALLOWED", "Agent runs require POST", declaration.id, null, { allow: "POST" });
1072
1073  if (segments.length === 2) {
1074    const schema = agentEndpointSchema(declaration, `${agentRunPrefix}${declaration.name}/runs`);
1075    return agentSession(request, declaration, schema, declaration.runCapability, environment, executionContext, async (principal) => {
1076      const input = await agentDecodeInput(declaration, request);
1077      const now = new Date().toISOString();
1078      const run = {
1079        schema: AGENT_RUN_RECORD_SCHEMA,
1080        runId: agentRunId(),
1081        agent: declaration.name,
1082        agentSemanticId: declaration.id,
1083        state: "Running",
1084        principal: principal.canonical,
1085        version: 0,
1086        input,
1087        turns: [],
1088        pending: null,
1089        output: null,
1090        error: null,
1091        startedAt: now,
1092        updatedAt: now,
1093      };
1094      await agentPersistRun(run);
1095      return { run };
1096    });
1097  }
1098
1099  const runId = segments[2];
1100  if (!AGENT_RUN_ID.test(runId)) return failure(400, "AGENT_RUN_NOT_FOUND", "The run id is not a run this build could have created", declaration.id);
1101  const schema = agentEndpointSchema(declaration, `${agentRunPrefix}${declaration.name}/runs/${runId}/resume`);
1102  return agentSession(request, declaration, schema, declaration.resumeCapability, environment, executionContext, async (principal, middlewareContext, middleware, signal) => {
1103    await __noxidReconcileAgentRuns().catch(() => {});
1104    let run;
1105    try { run = await agentLoadRun(declaration.name, runId); }
1106    catch (cause) { return { response: failure(409, cause?.code ?? "AGENT_RUN_RECORD_DRIFT", cause?.message ?? "The persisted run could not be read", declaration.id) }; }
1107    if (run === null) return { response: failure(404, "AGENT_RUN_NOT_FOUND", `Agent ${declaration.name} has no run ${runId}`, declaration.id) };
1108    // Authority before state: a run belongs to the principal that started it.
1109    // The stored canonical principal is the whole identity — the agent *and*
1110    // the user it acts for — so a second user holding
1111    // `agents.<name>.resume` cannot take over another user's paused run, and
1112    // nothing about the record is disclosed or written before this check.
1113    if (run.principal !== principal.canonical) {
1114      return { response: failure(403, "AGENT_RUN_PRINCIPAL_MISMATCH", `Agent ${declaration.name} run ${runId} was started by another principal, and only the principal that started a run resumes it; resume it as that principal, or start a new run as this one`, declaration.id, { agent: declaration.name, runId }) };
1115    }
1116    if (run.state !== "Paused" || run.pending === null) {
1117      // The conflict is structured for the same reason the acquisition
1118      // conflict is: a caller that arrives after the run finished should read
1119      // the outcome here rather than go looking for a second one. `output` is
1120      // the recorded answer of a completed run and null in every other state,
1121      // which is the only state that has one.
1122      return { response: failure(409, "AGENT_RUN_NOT_PAUSED", `Agent ${declaration.name} run ${runId} is ${run.state}, and only a paused run resumes`, declaration.id, { runId, state: run.state, output: run.state === "Completed" ? run.output ?? null : null }) };
1123    }
1124    const registry = agentToolRegistry(declaration);
1125    const entry = registry.get(run.pending.endpoint) ?? null;
1126    if (entry === null) {
1127      return { response: failure(409, "AGENT_RUN_TOOL_UNAVAILABLE", `The paused tool ${run.pending.endpoint} is no longer in this agent's registry`, declaration.id) };
1128    }
1129    // The deferred capability is re-checked with the *resuming* principal, not
1130    // the one that paused: approval is an act by whoever is resuming.
1131    const decision = await agentAuthorizeTool(declaration, entry, request, environment, executionContext, signal, middleware.route);
1132    if (decision.kind !== "allowed") {
1133      return { response: failure(403, "AGENT_PERMISSION_DENIED", `Capability ${run.pending.capability} is still not granted for agent ${declaration.name}`, declaration.id, { capability: run.pending.capability }) };
1134    }
1135    // The approved call is the first work of the resumed run: the pause froze
1136    // the turn immediately before dispatch, and `approved` carries the fresh
1137    // grant so the loop does not ask the authorizer a second time.
1138    // `principal` is never rewritten: it is the ownership record the check
1139    // above enforces, not a log of who touched the run last.
1140    const acquired = await agentAcquirePausedRun(run);
1141    if (acquired === null) {
1142      // The winner may still be between its own writes, so the reported state
1143      // is read with a bounded retry rather than guessed.
1144      let current = null;
1145      for (let attempt = 0; attempt < 3 && current === null; attempt += 1) {
1146        try { current = await agentLoadRun(declaration.name, runId); } catch { break; }
1147        if (current === null) await new Promise((resolve) => setTimeout(resolve, 5));
1148      }
1149      const state = current === null ? "Unknown" : current.state;
1150      return { response: failure(409, "AGENT_RUN_ACQUIRED", `Agent ${declaration.name} run ${runId} was claimed by another resume and is now ${state}; a paused run dispatches exactly once, so read that resume's stream instead of starting a second one`, declaration.id, { runId, state, output: current?.output ?? null }) };
1151    }
1152    return { run: acquired };
1153  });
1154}
1155"##;