@elevasis/sdk 1.42.0 → 1.44.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.cjs +3699 -3280
- package/dist/index.d.ts +914 -627
- package/dist/index.js +3826 -3407
- package/dist/node/index.d.ts +788 -508
- package/dist/test-utils/index.d.ts +800 -519
- package/dist/test-utils/index.js +1390 -1049
- package/dist/types/worker/adapters/llm.d.ts +2 -4
- package/dist/types/worker/index.d.ts +4 -1
- package/dist/worker/index.js +1048 -1040
- package/package.json +2 -2
- package/reference/claude-config/sync-notes/2026-07-30-login-screen-and-member-provisioning-state.md +114 -0
- package/reference/sdk/platform-tools/index.mdx +5 -6
- package/reference/sdk/resources/index.mdx +46 -11
- package/reference/sdk/resources/types.mdx +14 -14
package/dist/test-utils/index.js
CHANGED
|
@@ -4460,21 +4460,16 @@ function resolveSecurityLevel(config2) {
|
|
|
4460
4460
|
}
|
|
4461
4461
|
|
|
4462
4462
|
// ../core/src/execution/engine/agent/reasoning/prompt-sections/base-actions.ts
|
|
4463
|
-
function buildBaseActionsPrompt(
|
|
4464
|
-
const actionNames = ["tool-call (call a tool)"];
|
|
4465
|
-
if (includeNavigateKnowledge) {
|
|
4466
|
-
actionNames.push("navigate-knowledge (load knowledge node)");
|
|
4467
|
-
}
|
|
4468
|
-
actionNames.push("complete (finish task)");
|
|
4469
|
-
const actionsList = actionNames.map((name, index2) => `${index2 + 1}. ${name}`).join("\n");
|
|
4470
|
-
const actionCount = actionNames.length;
|
|
4463
|
+
function buildBaseActionsPrompt(includeMessage) {
|
|
4471
4464
|
return `# CORE AGENT INSTRUCTIONS
|
|
4472
4465
|
|
|
4473
|
-
You are an AI agent. Your response is captured as structured output. ${
|
|
4466
|
+
You are an AI agent. Your response is captured as structured output. ${includeMessage ? "Three fields are required" : "Two fields are required"} on
|
|
4474
4467
|
every response:
|
|
4475
4468
|
|
|
4476
4469
|
- **reasoning** -- your thought process, as plain prose.
|
|
4477
|
-
- **nextActions** -- the actions to execute
|
|
4470
|
+
- **nextActions** -- the actions to execute: \`tool-call\` to call a tool, or \`complete\` to finish. Tool calls
|
|
4471
|
+
batched into the same iteration run in parallel, and their results appear in your next iteration; without a
|
|
4472
|
+
\`complete\` action, the system iterates again.${includeMessage ? `
|
|
4478
4473
|
- **message** -- your reply to the user, as plain prose. This is the ONLY field the user sees.
|
|
4479
4474
|
Whatever you decide to say, it goes here. Use an empty string only when this iteration just calls
|
|
4480
4475
|
tools and you genuinely have nothing to tell the user yet -- an empty message ends the turn in
|
|
@@ -4484,141 +4479,18 @@ every response:
|
|
|
4484
4479
|
and never continue the response envelope in the reasoning text -- nextActions is a separate field
|
|
4485
4480
|
that you fill separately. A response carrying reasoning alone is discarded and retried.
|
|
4486
4481
|
|
|
4487
|
-
## Action Types (${actionCount} available)
|
|
4488
|
-
|
|
4489
|
-
${actionsList}
|
|
4490
|
-
|
|
4491
|
-
**Formats:**
|
|
4492
|
-
- tool-call: { "type": "tool-call", "id": "unique-id", "name": "tool_name", "input": {...} }${includeNavigateKnowledge ? `
|
|
4493
|
-
- navigate-knowledge: { "type": "navigate-knowledge", "id": "unique-id", "nodeId": "node-name" }` : ""}
|
|
4494
|
-
- complete: { "type": "complete" }
|
|
4495
|
-
${includeMessageAction ? `
|
|
4496
|
-
Talking to the user is NOT an action. There is no message action -- put your reply in the
|
|
4497
|
-
**message** field beside nextActions.
|
|
4498
|
-
` : ""}
|
|
4499
|
-
## Execution Flow
|
|
4500
|
-
|
|
4501
|
-
1. You respond with reasoning + actions${includeMessageAction ? " + your message to the user" : ""}
|
|
4502
|
-
2. System executes actions (tool calls run **in parallel**)
|
|
4503
|
-
3. Tool results automatically appear in your next iteration
|
|
4504
|
-
4. You see results and decide: more work needed? Or complete?
|
|
4505
|
-
5. **Without "complete" action, system iterates again**
|
|
4506
|
-
|
|
4507
4482
|
## Rules
|
|
4508
4483
|
|
|
4509
4484
|
- Batch independent tool calls in one iteration (faster execution)
|
|
4510
|
-
- Dependent operations need separate iterations
|
|
4511
|
-
- "complete"
|
|
4512
|
-
-
|
|
4485
|
+
- Dependent operations need separate iterations -- e.g. look up a record before updating it, once the update needs a value only the lookup returns
|
|
4486
|
+
- "complete" can be included alongside tool calls in the same iteration -- the tools still run and you still see their results next iteration before the turn actually ends, so there is no need to withhold it while a call is pending
|
|
4487
|
+
- Complete when the task finished successfully, a tool returned empty/error results (inform the user first), or you need user input to proceed (ask the question first)
|
|
4488
|
+
- Don't complete when you just called a tool and need its results, or more iterations are needed${includeMessage ? `
|
|
4513
4489
|
- Always fill message before completing -- it is the only field the user sees, and completing with it empty ends the turn in silence
|
|
4514
4490
|
- message holds one reply. Write the whole reply in it; do not split a reply across iterations
|
|
4515
4491
|
- When you have your answer, put it in message and include complete in the SAME iteration. Never reply on one iteration then complete on a later one
|
|
4516
4492
|
- Never repeat or rephrase the same answer across iterations. One clear answer, then complete` : ""}
|
|
4517
|
-
|
|
4518
|
-
**Use "complete" when:**
|
|
4519
|
-
- Task finished successfully
|
|
4520
|
-
- Tool returned empty/error results (inform user first)
|
|
4521
|
-
- You need user input to proceed (ask question first)
|
|
4522
|
-
|
|
4523
|
-
**Don't use "complete" when:**
|
|
4524
|
-
- You just called a tool and need its results
|
|
4525
|
-
- You used navigate-knowledge and need the newly loaded knowledge in the next iteration
|
|
4526
|
-
- More iterations are needed
|
|
4527
|
-
|
|
4528
|
-
## Examples
|
|
4529
|
-
|
|
4530
|
-
Each example shows the field values, not a JSON document to copy.
|
|
4531
|
-
|
|
4532
|
-
### Example 1: Simple Task (No Tools)
|
|
4533
|
-
- reasoning: Simple greeting, no tools needed.${includeMessageAction ? "\n- message: Hi! How can I help?" : ""}
|
|
4534
|
-
- nextActions: [{ "type": "complete" }]
|
|
4535
|
-
|
|
4536
|
-
### Example 2: Tool Usage (Two Iterations)
|
|
4537
|
-
|
|
4538
|
-
**Iteration 1 - Call tool (NO complete - waiting for results):**
|
|
4539
|
-
- reasoning: User asked for time. Calling get_time tool.${includeMessageAction ? "\n- message: Checking the time..." : ""}
|
|
4540
|
-
- nextActions: [{ "type": "tool-call", "id": "t1", "name": "get_time", "input": { "timezone": "UTC" } }]
|
|
4541
|
-
|
|
4542
|
-
**Iteration 2 - Tool result received, now complete:**
|
|
4543
|
-
- reasoning: Got time result: 12:00 PM UTC. Task done.${includeMessageAction ? "\n- message: The current time is 12:00 PM UTC." : ""}
|
|
4544
|
-
- nextActions: [{ "type": "complete" }]
|
|
4545
|
-
|
|
4546
|
-
### Example 3: Parallel Tool Calls (Independent Operations)
|
|
4547
|
-
When tools don't depend on each other, batch them for faster execution.
|
|
4548
|
-
|
|
4549
|
-
- reasoning: User wants time AND weather. Independent operations - calling both in parallel.${includeMessageAction ? "\n- message: Getting time and weather..." : ""}
|
|
4550
|
-
- nextActions: [{ "type": "tool-call", "id": "t1", "name": "get_time", "input": {} }, { "type": "tool-call", "id": "w1", "name": "get_weather", "input": { "city": "NYC" } }]
|
|
4551
|
-
|
|
4552
|
-
### Example 4: Dependent Operations (Separate Iterations Required)
|
|
4553
|
-
|
|
4554
|
-
**\u274C WRONG - Cannot batch dependent operations:**
|
|
4555
|
-
- nextActions: [{ "type": "tool-call", "id": "1", "name": "search_user", "input": { "email": "user@example.com" } }, { "type": "tool-call", "id": "2", "name": "update_user", "input": { "userId": "???" } }]
|
|
4556
|
-
|
|
4557
|
-
Problem: update_user needs userId from search_user result!
|
|
4558
|
-
|
|
4559
|
-
**\u2705 CORRECT - Iteration 1 (get the dependency):**
|
|
4560
|
-
- reasoning: Need to find user first before updating.${includeMessageAction ? "\n- message: Looking up user..." : ""}
|
|
4561
|
-
- nextActions: [{ "type": "tool-call", "id": "1", "name": "search_user", "input": { "email": "user@example.com" } }]
|
|
4562
|
-
|
|
4563
|
-
**\u2705 CORRECT - Iteration 2 (use the result):**
|
|
4564
|
-
- reasoning: Found userId: user_123. Now can update.
|
|
4565
|
-
- nextActions: [{ "type": "tool-call", "id": "2", "name": "update_user", "input": { "userId": "user_123", "name": "New Name" } }]
|
|
4566
|
-
|
|
4567
|
-
---
|
|
4568
|
-
|
|
4569
|
-
These are your CORE INSTRUCTIONS. Additional context follows below.
|
|
4570
|
-
`;
|
|
4571
|
-
}
|
|
4572
|
-
|
|
4573
|
-
// ../core/src/execution/engine/agent/reasoning/prompt-sections/knowledge-map.ts
|
|
4574
|
-
function buildKnowledgeMapPrompt(knowledgeMap) {
|
|
4575
|
-
if (!knowledgeMap || Object.keys(knowledgeMap.nodes).length === 0) {
|
|
4576
|
-
return "";
|
|
4577
|
-
}
|
|
4578
|
-
let section = "## Knowledge Map\n\n";
|
|
4579
|
-
section += "Knowledge maps provide on-demand access to specialized capabilities. ";
|
|
4580
|
-
section += "Each node contains domain-specific instructions and tools.\n\n";
|
|
4581
|
-
section += "**CRITICAL**: After navigating to a node, tools become available in the **NEXT iteration**. ";
|
|
4582
|
-
section += "Do NOT attempt to use tools in the same iteration as navigation.\n\n";
|
|
4583
|
-
const loadedNodes = [];
|
|
4584
|
-
const unloadedNodes = [];
|
|
4585
|
-
Object.values(knowledgeMap.nodes).forEach((node) => {
|
|
4586
|
-
if (node.loaded && node.prompt) {
|
|
4587
|
-
loadedNodes.push(node);
|
|
4588
|
-
} else {
|
|
4589
|
-
unloadedNodes.push(node);
|
|
4590
|
-
}
|
|
4591
|
-
});
|
|
4592
|
-
if (loadedNodes.length > 0) {
|
|
4593
|
-
section += "### Loaded Knowledge\n\n";
|
|
4594
|
-
section += "These nodes are active - their tools are available now:\n\n";
|
|
4595
|
-
loadedNodes.forEach((node) => {
|
|
4596
|
-
section += `**${node.id}**
|
|
4597
|
-
${node.prompt}
|
|
4598
|
-
|
|
4599
|
-
`;
|
|
4600
|
-
});
|
|
4601
|
-
}
|
|
4602
|
-
if (unloadedNodes.length > 0) {
|
|
4603
|
-
section += "### Available to Load\n\n";
|
|
4604
|
-
unloadedNodes.forEach((node) => {
|
|
4605
|
-
section += `- **${node.id}**: ${node.description}
|
|
4606
4493
|
`;
|
|
4607
|
-
});
|
|
4608
|
-
section += "\n### How to Navigate\n\n";
|
|
4609
|
-
section += "Put a navigate-knowledge entry in your nextActions:\n";
|
|
4610
|
-
section += '`{ "type": "navigate-knowledge", "id": "unique-id", "nodeId": "node-id" }`\n\n';
|
|
4611
|
-
section += "### Typical Workflow\n\n";
|
|
4612
|
-
section += "**Iteration 1 - Navigate to load knowledge:**\n";
|
|
4613
|
-
section += "- reasoning: I need [domain] capabilities to accomplish this task.\n";
|
|
4614
|
-
section += '- nextActions: [{ "type": "navigate-knowledge", "id": "nav-1", "nodeId": "[node-id]" }]\n\n';
|
|
4615
|
-
section += "**Iteration 2 - Use newly available tools:**\n";
|
|
4616
|
-
section += "- reasoning: Now I have [domain] tools. Using [tool_name] to [action].\n";
|
|
4617
|
-
section += '- nextActions: [{ "type": "tool-call", "id": "t1", "name": "[tool_name]", "input": { ... } }]\n\n';
|
|
4618
|
-
section += "**Note:** Loaded knowledge persists across conversation turns. ";
|
|
4619
|
-
section += "Previously loaded nodes remain available without re-navigation.\n";
|
|
4620
|
-
}
|
|
4621
|
-
return section + "\n";
|
|
4622
4494
|
}
|
|
4623
4495
|
|
|
4624
4496
|
// ../core/src/execution/engine/agent/reasoning/prompt-sections/tools.ts
|
|
@@ -4626,86 +4498,20 @@ function buildToolsPrompt(tools) {
|
|
|
4626
4498
|
if (tools.length === 0) {
|
|
4627
4499
|
return "";
|
|
4628
4500
|
}
|
|
4629
|
-
|
|
4630
|
-
|
|
4631
|
-
tools.forEach((tool) => {
|
|
4632
|
-
section += `### ${tool.name}
|
|
4633
|
-
`;
|
|
4634
|
-
section += `${tool.description}
|
|
4635
|
-
`;
|
|
4636
|
-
section += `Input schema: ${JSON.stringify(tool.inputSchema, null, 2)}
|
|
4637
|
-
|
|
4638
|
-
`;
|
|
4639
|
-
});
|
|
4640
|
-
section += "To call a tool, return a tool-call action:\n";
|
|
4641
|
-
section += '{\n "type": "tool-call",\n "id": "unique-id",\n "name": "tool-name",\n "input": { /* tool input matching schema */ }\n}\n\n';
|
|
4642
|
-
section += "**IMPORTANT RULES:**\n";
|
|
4643
|
-
section += '1. "complete" CANNOT mix with navigate-knowledge actions in the same response\n';
|
|
4644
|
-
section += '2. The "message" field CAN be filled on the same response that completes - always pair your final message with complete in the same iteration\n';
|
|
4645
|
-
section += '3. "complete" CAN mix with fire-and-forget tool-call actions when you do not need their results\n';
|
|
4646
|
-
section += "4. To use tools and inspect their results, return ONLY tool-call actions, then wait for results in the next iteration\n";
|
|
4647
|
-
section += "5. After receiving tool results, you can either call more tools OR complete with final answer\n";
|
|
4648
|
-
section += "6. navigate-knowledge actions load new capabilities - tools become available in the next iteration\n";
|
|
4649
|
-
return section + "\n";
|
|
4650
|
-
}
|
|
4651
|
-
|
|
4652
|
-
// ../core/src/execution/engine/agent/reasoning/prompt-sections/memory.ts
|
|
4653
|
-
function buildMemoryPrompt(memoryStatus, preferences) {
|
|
4654
|
-
return `## Memory Management
|
|
4655
|
-
|
|
4656
|
-
You have control over session memory. Use memoryOps to manage critical information:
|
|
4657
|
-
|
|
4658
|
-
\`memoryOps\` is a field of your structured response, not a document you write out. Its \`set\` is a
|
|
4659
|
-
LIST of entries, each with a \`key\` and a \`value\` \u2014 not an object keyed by name.
|
|
4660
|
-
|
|
4661
|
-
**SET critical information** \u2014 \`set\` entries look like:
|
|
4662
|
-
|
|
4663
|
-
- key \`customer_account\`, value \`Account #12345, Premium tier, expires 2026-03-15\`
|
|
4664
|
-
- key \`original_request\`, value \`Fix broken widget\`
|
|
4665
|
-
|
|
4666
|
-
**DELETE outdated information** \u2014 \`delete\` is a list of key names:
|
|
4667
|
-
|
|
4668
|
-
- \`old_address\`, \`cancelled_order\`
|
|
4669
|
-
|
|
4670
|
-
**When to persist:**
|
|
4671
|
-
- Memory at ${memoryStatus.historyPercent}%: ${memoryStatus.historyPercent >= 80 ? "Proactively persist important context NOW (auto-compaction at 100%)" : "Normal operation"}
|
|
4672
|
-
- Session keys at ${memoryStatus.sessionMemoryKeys}/${memoryStatus.sessionMemoryLimit}: Delete outdated keys before adding new ones
|
|
4673
|
-
- Always: Persist critical data that should survive memory compaction
|
|
4674
|
-
|
|
4675
|
-
**IMPORTANT - System-Managed Memory:**
|
|
4676
|
-
Do NOT update these keys via memoryOps (managed automatically by tools/actions):
|
|
4677
|
-
- notion-pages-cache (managed by Notion tools)
|
|
4678
|
-
- knowledge-map-state (managed by navigate-knowledge)
|
|
4679
|
-
|
|
4680
|
-
Attempting to update system-managed keys will be rejected. Use tools to update their caches.
|
|
4681
|
-
${preferences ? `
|
|
4682
|
-
**Agent-Specific Guidance:**
|
|
4683
|
-
${preferences}
|
|
4684
|
-
` : ""}
|
|
4685
|
-
Framework auto-compacts history at 100% token budget.
|
|
4686
|
-
You control WHAT to remember. Framework controls HOW compaction works.
|
|
4687
|
-
|
|
4688
|
-
`;
|
|
4501
|
+
return tools.map((tool) => `### ${tool.name}
|
|
4502
|
+
${tool.description}`).join("\n\n") + "\n";
|
|
4689
4503
|
}
|
|
4690
4504
|
|
|
4691
4505
|
// ../core/src/execution/engine/agent/reasoning/prompt-sections/completion.ts
|
|
4692
4506
|
function buildCompletionPrompt(outputSchema) {
|
|
4693
|
-
|
|
4694
|
-
|
|
4695
|
-
section += '```json\n{ "type": "complete" }\n```\n\n';
|
|
4696
|
-
if (outputSchema) {
|
|
4697
|
-
section += "After task completion, the final output will be generated and will need to include:\n";
|
|
4698
|
-
section += describeOutputSchema(outputSchema);
|
|
4699
|
-
section += "\n\nDuring task execution, focus on gathering all necessary information.";
|
|
4700
|
-
} else {
|
|
4701
|
-
section += "This is a side-effect agent (no output generation). Focus on performing the requested actions.";
|
|
4507
|
+
if (!outputSchema) {
|
|
4508
|
+
return "";
|
|
4702
4509
|
}
|
|
4703
|
-
return
|
|
4510
|
+
return "When you complete the task, the final output will be generated and will need to include:\n" + describeOutputSchema(outputSchema) + "\n\nDuring task execution, focus on gathering all necessary information.\n";
|
|
4704
4511
|
}
|
|
4705
4512
|
function describeOutputSchema(schema) {
|
|
4706
4513
|
const jsonSchema = zodToJsonSchema(schema, {
|
|
4707
|
-
$refStrategy: "none"
|
|
4708
|
-
errorMessages: true
|
|
4514
|
+
$refStrategy: "none"
|
|
4709
4515
|
});
|
|
4710
4516
|
return "```json\n" + JSON.stringify(jsonSchema, null, 2) + "\n```";
|
|
4711
4517
|
}
|
|
@@ -4717,53 +4523,82 @@ function buildSystemPrompt(agentPrompt, options) {
|
|
|
4717
4523
|
if (securitySection) {
|
|
4718
4524
|
sections.push(securitySection);
|
|
4719
4525
|
}
|
|
4720
|
-
sections.push(buildBaseActionsPrompt(options.
|
|
4721
|
-
const knowledgeMapSection = buildKnowledgeMapPrompt(options.knowledgeMap);
|
|
4722
|
-
if (knowledgeMapSection) {
|
|
4723
|
-
sections.push(knowledgeMapSection);
|
|
4724
|
-
}
|
|
4526
|
+
sections.push(buildBaseActionsPrompt(options.capabilities.message !== "off"));
|
|
4725
4527
|
const toolsSection = buildToolsPrompt(options.tools);
|
|
4726
4528
|
if (toolsSection) {
|
|
4727
4529
|
sections.push(toolsSection);
|
|
4728
4530
|
}
|
|
4729
|
-
|
|
4730
|
-
|
|
4531
|
+
const completionSection = buildCompletionPrompt(options.outputSchema);
|
|
4532
|
+
if (completionSection) {
|
|
4533
|
+
sections.push(completionSection);
|
|
4731
4534
|
}
|
|
4732
|
-
sections.push(buildCompletionPrompt(options.outputSchema));
|
|
4733
4535
|
sections.push("---\n");
|
|
4734
4536
|
sections.push("# AGENT-SPECIFIC INSTRUCTIONS\n\n");
|
|
4735
|
-
sections.push(
|
|
4537
|
+
sections.push(
|
|
4538
|
+
options.memoryPreferences ? `${agentPrompt}
|
|
4539
|
+
|
|
4540
|
+
**Agent-Specific Memory Guidance:**
|
|
4541
|
+
${options.memoryPreferences}
|
|
4542
|
+
` : agentPrompt
|
|
4543
|
+
);
|
|
4736
4544
|
return sections.join("\n");
|
|
4737
4545
|
}
|
|
4546
|
+
var toolInputSchemaCache = /* @__PURE__ */ new WeakMap();
|
|
4547
|
+
function getToolInputSchema(tool) {
|
|
4548
|
+
let schema = toolInputSchemaCache.get(tool);
|
|
4549
|
+
if (schema === void 0) {
|
|
4550
|
+
schema = zodToJsonSchema(tool.inputSchema, { $refStrategy: "none" });
|
|
4551
|
+
toolInputSchemaCache.set(tool, schema);
|
|
4552
|
+
}
|
|
4553
|
+
return schema;
|
|
4554
|
+
}
|
|
4555
|
+
var reasoningRequestCache = /* @__PURE__ */ new WeakMap();
|
|
4738
4556
|
function buildReasoningRequest(iterationContext) {
|
|
4739
|
-
const tools = Array.from(iterationContext.toolRegistry.values());
|
|
4740
|
-
const toolDefinitions = tools.map((tool) => ({
|
|
4741
|
-
name: tool.name,
|
|
4742
|
-
description: tool.description,
|
|
4743
|
-
inputSchema: zodToJsonSchema(tool.inputSchema)
|
|
4744
|
-
}));
|
|
4745
|
-
const memoryStatus = iterationContext.memoryManager.getStatus();
|
|
4746
|
-
const isSessionCapable = !!iterationContext.config.sessionCapable;
|
|
4747
|
-
const hasKnowledgeMap = !!(iterationContext.knowledgeMap && Object.keys(iterationContext.knowledgeMap.nodes).length > 0);
|
|
4748
|
-
const includeMemoryOps = !!iterationContext.config.memoryPreferences;
|
|
4749
|
-
const securityLevel = resolveSecurityLevel(iterationContext.config);
|
|
4750
|
-
const systemPrompt = buildSystemPrompt(iterationContext.config.systemPrompt, {
|
|
4751
|
-
securityLevel,
|
|
4752
|
-
includeMessageAction: isSessionCapable,
|
|
4753
|
-
includeNavigateKnowledge: hasKnowledgeMap,
|
|
4754
|
-
knowledgeMap: iterationContext.knowledgeMap,
|
|
4755
|
-
tools: toolDefinitions,
|
|
4756
|
-
memoryStatus,
|
|
4757
|
-
outputSchema: iterationContext.contract.outputSchema,
|
|
4758
|
-
memoryPreferences: iterationContext.config.memoryPreferences
|
|
4759
|
-
});
|
|
4760
4557
|
iterationContext.memoryManager.enforceHardLimits();
|
|
4558
|
+
const capabilities = {
|
|
4559
|
+
// Non-session agents get 'off' -- message stays absent from their schema entirely, same as
|
|
4560
|
+
// before this was a tri-state. Session-capable agents default to 'required' (decision B1); an
|
|
4561
|
+
// agent can opt into 'optional' via `messagePolicy`. `AgentKind` deliberately plays no part
|
|
4562
|
+
// here -- the most conversational agent on the platform is `kind: 'platform'`.
|
|
4563
|
+
message: iterationContext.config.sessionCapable ? iterationContext.config.messagePolicy ?? "required" : "off",
|
|
4564
|
+
// memoryOps is available whenever the agent declared memory preferences.
|
|
4565
|
+
memoryOps: !!iterationContext.config.memoryPreferences
|
|
4566
|
+
};
|
|
4567
|
+
const securityLevel = resolveSecurityLevel(iterationContext.config);
|
|
4568
|
+
const registrySize = iterationContext.toolRegistry.size;
|
|
4569
|
+
const cached = reasoningRequestCache.get(iterationContext.toolRegistry);
|
|
4570
|
+
let toolDefinitions;
|
|
4571
|
+
let systemPrompt;
|
|
4572
|
+
if (cached && cached.registrySize === registrySize) {
|
|
4573
|
+
toolDefinitions = cached.toolDefinitions;
|
|
4574
|
+
systemPrompt = cached.systemPrompt;
|
|
4575
|
+
} else {
|
|
4576
|
+
const tools = Array.from(iterationContext.toolRegistry.values());
|
|
4577
|
+
toolDefinitions = tools.map((tool) => ({
|
|
4578
|
+
name: tool.name,
|
|
4579
|
+
description: tool.description,
|
|
4580
|
+
inputSchema: getToolInputSchema(tool)
|
|
4581
|
+
}));
|
|
4582
|
+
systemPrompt = buildSystemPrompt(iterationContext.config.systemPrompt, {
|
|
4583
|
+
securityLevel,
|
|
4584
|
+
capabilities,
|
|
4585
|
+
tools: toolDefinitions,
|
|
4586
|
+
outputSchema: iterationContext.contract.outputSchema,
|
|
4587
|
+
memoryPreferences: iterationContext.config.memoryPreferences
|
|
4588
|
+
});
|
|
4589
|
+
reasoningRequestCache.set(iterationContext.toolRegistry, { registrySize, toolDefinitions, systemPrompt });
|
|
4590
|
+
}
|
|
4761
4591
|
return {
|
|
4762
4592
|
systemPrompt,
|
|
4763
4593
|
tools: toolDefinitions,
|
|
4764
4594
|
constraints: {
|
|
4765
4595
|
maxOutputTokens: iterationContext.modelConfig.maxOutputTokens,
|
|
4766
|
-
|
|
4596
|
+
// Matches the completion phase's own default (`agent.ts`'s `generateFinalOutput`). Inert on
|
|
4597
|
+
// every Claude 5 model today -- `getSamplingParameters` in the Anthropic adapter drops
|
|
4598
|
+
// `temperature` entirely for any model not on its sampling allowlist -- but it reaches
|
|
4599
|
+
// Haiku 4.5, OpenAI, OpenRouter, and Google, where the hardcoded `1` was silently overriding
|
|
4600
|
+
// whatever the tenant configured.
|
|
4601
|
+
temperature: iterationContext.modelConfig.temperature ?? 0.7
|
|
4767
4602
|
},
|
|
4768
4603
|
memory: iterationContext.memoryManager.toContextParts(
|
|
4769
4604
|
iterationContext.iteration,
|
|
@@ -4773,14 +4608,13 @@ function buildReasoningRequest(iterationContext) {
|
|
|
4773
4608
|
securityLevel,
|
|
4774
4609
|
// A session agent gets its own conversation. Non-session executions have none.
|
|
4775
4610
|
conversationHistory: iterationContext.executionContext.conversationHistory ?? [],
|
|
4776
|
-
|
|
4777
|
-
includeNavigateKnowledge: hasKnowledgeMap,
|
|
4778
|
-
includeMemoryOps
|
|
4611
|
+
capabilities
|
|
4779
4612
|
};
|
|
4780
4613
|
}
|
|
4781
4614
|
var ToolCallActionSchema = z.object({
|
|
4782
4615
|
type: z.literal("tool-call"),
|
|
4783
|
-
id: z.string(),
|
|
4616
|
+
id: z.string().optional(),
|
|
4617
|
+
// Optional: no longer in the grammar (B8); still-deployed bundles may send it
|
|
4784
4618
|
name: z.string(),
|
|
4785
4619
|
input: z.any()
|
|
4786
4620
|
// Use z.any() instead of z.unknown() for JSON Schema compatibility
|
|
@@ -4792,17 +4626,56 @@ var MessageActionSchema = z.object({
|
|
|
4792
4626
|
type: z.literal("message"),
|
|
4793
4627
|
text: z.string()
|
|
4794
4628
|
});
|
|
4795
|
-
var NavigateKnowledgeActionSchema = z.object({
|
|
4796
|
-
type: z.literal("navigate-knowledge"),
|
|
4797
|
-
id: z.string(),
|
|
4798
|
-
nodeId: z.string()
|
|
4799
|
-
});
|
|
4800
4629
|
var AgentActionSchema = z.discriminatedUnion("type", [
|
|
4801
4630
|
ToolCallActionSchema,
|
|
4802
4631
|
CompleteActionSchema,
|
|
4803
|
-
MessageActionSchema
|
|
4804
|
-
NavigateKnowledgeActionSchema
|
|
4632
|
+
MessageActionSchema
|
|
4805
4633
|
]);
|
|
4634
|
+
|
|
4635
|
+
// ../core/src/execution/engine/llm/errors.ts
|
|
4636
|
+
var LLMError = class extends ExecutionError {
|
|
4637
|
+
type = "llm_error";
|
|
4638
|
+
severity = "warning";
|
|
4639
|
+
category = "llm";
|
|
4640
|
+
constructor(message, context) {
|
|
4641
|
+
super(message, context);
|
|
4642
|
+
}
|
|
4643
|
+
};
|
|
4644
|
+
var InsufficientTokensError = class extends LLMError {
|
|
4645
|
+
type = "insufficient_tokens";
|
|
4646
|
+
severity = "critical";
|
|
4647
|
+
constructor(message, context) {
|
|
4648
|
+
super(message, context);
|
|
4649
|
+
}
|
|
4650
|
+
/** The model configuration is short of what the request needs; retrying sends the identical
|
|
4651
|
+
* request into the identical shortfall. */
|
|
4652
|
+
isRetryable() {
|
|
4653
|
+
return false;
|
|
4654
|
+
}
|
|
4655
|
+
};
|
|
4656
|
+
var LLMResponseParseError = class extends LLMError {
|
|
4657
|
+
type = "llm_response_parse_error";
|
|
4658
|
+
severity = "warning";
|
|
4659
|
+
constructor(message, context) {
|
|
4660
|
+
super(message, context);
|
|
4661
|
+
}
|
|
4662
|
+
/** JSON parse failures are transient LLM errors -- the same prompt can produce well-formed JSON on
|
|
4663
|
+
* the next attempt. This is also the one `isRetryable()` verdict `isRetryableError` had to special-case
|
|
4664
|
+
* ahead of everything else before it consulted the typed contract at all. */
|
|
4665
|
+
isRetryable() {
|
|
4666
|
+
return true;
|
|
4667
|
+
}
|
|
4668
|
+
};
|
|
4669
|
+
var ModelConfigError = class extends ExecutionError {
|
|
4670
|
+
constructor(message, field, model, context) {
|
|
4671
|
+
super(message, { ...context, field, model });
|
|
4672
|
+
this.field = field;
|
|
4673
|
+
this.model = model;
|
|
4674
|
+
}
|
|
4675
|
+
type = "model_config_error";
|
|
4676
|
+
severity = "warning";
|
|
4677
|
+
category = "validation";
|
|
4678
|
+
};
|
|
4806
4679
|
var GPT5OptionsSchema = z.object({
|
|
4807
4680
|
reasoning_effort: z.enum(["minimal", "low", "medium", "high"]).optional(),
|
|
4808
4681
|
verbosity: z.enum(["low", "medium", "high"]).optional()
|
|
@@ -4842,19 +4715,6 @@ var OpenRouterConfigSchema = z.object({
|
|
|
4842
4715
|
topP: z.number().min(0).max(1).optional(),
|
|
4843
4716
|
modelOptions: OpenRouterOptionsSchema.optional()
|
|
4844
4717
|
});
|
|
4845
|
-
var GoogleOptionsSchema = z.object({
|
|
4846
|
-
/** Thinking level for Gemini 3 models (controls reasoning depth) */
|
|
4847
|
-
thinkingLevel: z.enum(["minimal", "low", "medium", "high"]).optional()
|
|
4848
|
-
});
|
|
4849
|
-
var GoogleConfigSchema = z.object({
|
|
4850
|
-
model: z.enum(["gemini-3-flash-preview", "gemini-3.1-flash-lite-preview"]),
|
|
4851
|
-
provider: z.literal("google"),
|
|
4852
|
-
apiKey: z.string(),
|
|
4853
|
-
temperature: z.number().min(0).max(2).optional(),
|
|
4854
|
-
maxOutputTokens: z.number().min(500).optional(),
|
|
4855
|
-
topP: z.number().min(0).max(1).optional(),
|
|
4856
|
-
modelOptions: GoogleOptionsSchema.optional()
|
|
4857
|
-
});
|
|
4858
4718
|
var AnthropicOptionsSchema = z.object({}).strict();
|
|
4859
4719
|
var AnthropicStandardConfigSchema = z.object({
|
|
4860
4720
|
model: z.enum(["claude-haiku-4-5-20251001", "claude-haiku-4-5"]),
|
|
@@ -4948,31 +4808,6 @@ var MODEL_INFO = {
|
|
|
4948
4808
|
category: "standard",
|
|
4949
4809
|
configSchema: OpenRouterConfigSchema
|
|
4950
4810
|
},
|
|
4951
|
-
// Google Gemini Models (direct SDK access via @google/genai)
|
|
4952
|
-
"gemini-3-flash-preview": {
|
|
4953
|
-
inputCostPer1M: 50,
|
|
4954
|
-
// $0.50 per 1M tokens
|
|
4955
|
-
outputCostPer1M: 300,
|
|
4956
|
-
// $3.00 per 1M tokens
|
|
4957
|
-
minTokens: 4e3,
|
|
4958
|
-
recommendedTokens: 8e3,
|
|
4959
|
-
maxTokens: 1e6,
|
|
4960
|
-
// 1M context window
|
|
4961
|
-
category: "standard",
|
|
4962
|
-
configSchema: GoogleConfigSchema
|
|
4963
|
-
},
|
|
4964
|
-
"gemini-3.1-flash-lite-preview": {
|
|
4965
|
-
inputCostPer1M: 25,
|
|
4966
|
-
// $0.25 per 1M tokens
|
|
4967
|
-
outputCostPer1M: 150,
|
|
4968
|
-
// $1.50 per 1M tokens
|
|
4969
|
-
minTokens: 4e3,
|
|
4970
|
-
recommendedTokens: 8e3,
|
|
4971
|
-
maxTokens: 1e6,
|
|
4972
|
-
// 1M context window
|
|
4973
|
-
category: "standard",
|
|
4974
|
-
configSchema: GoogleConfigSchema
|
|
4975
|
-
},
|
|
4976
4811
|
// Anthropic Claude Models (direct SDK access via @anthropic-ai/sdk)
|
|
4977
4812
|
"claude-opus-5": {
|
|
4978
4813
|
inputCostPer1M: 500,
|
|
@@ -5029,50 +4864,18 @@ var MODEL_INFO = {
|
|
|
5029
4864
|
configSchema: AnthropicConfigSchema
|
|
5030
4865
|
}
|
|
5031
4866
|
};
|
|
4867
|
+
var MODEL_KEYS_BY_SPECIFICITY = Object.keys(MODEL_INFO).sort((a3, b2) => b2.length - a3.length);
|
|
5032
4868
|
function getModelInfo(model) {
|
|
5033
4869
|
if (model in MODEL_INFO) {
|
|
5034
4870
|
return MODEL_INFO[model];
|
|
5035
4871
|
}
|
|
5036
|
-
for (const
|
|
4872
|
+
for (const knownModel of MODEL_KEYS_BY_SPECIFICITY) {
|
|
5037
4873
|
if (model.startsWith(knownModel)) {
|
|
5038
|
-
return
|
|
4874
|
+
return MODEL_INFO[knownModel];
|
|
5039
4875
|
}
|
|
5040
4876
|
}
|
|
5041
4877
|
return void 0;
|
|
5042
4878
|
}
|
|
5043
|
-
|
|
5044
|
-
// ../core/src/execution/engine/llm/errors.ts
|
|
5045
|
-
var LLMError = class extends ExecutionError {
|
|
5046
|
-
type = "llm_error";
|
|
5047
|
-
severity = "warning";
|
|
5048
|
-
category = "llm";
|
|
5049
|
-
constructor(message, context) {
|
|
5050
|
-
super(message, context);
|
|
5051
|
-
}
|
|
5052
|
-
isRetryable() {
|
|
5053
|
-
return true;
|
|
5054
|
-
}
|
|
5055
|
-
};
|
|
5056
|
-
var InsufficientTokensError = class extends LLMError {
|
|
5057
|
-
type = "insufficient_tokens";
|
|
5058
|
-
severity = "critical";
|
|
5059
|
-
constructor(message, context) {
|
|
5060
|
-
super(message, context);
|
|
5061
|
-
}
|
|
5062
|
-
isRetryable() {
|
|
5063
|
-
return false;
|
|
5064
|
-
}
|
|
5065
|
-
};
|
|
5066
|
-
var ModelConfigError = class extends ExecutionError {
|
|
5067
|
-
constructor(message, field, model, context) {
|
|
5068
|
-
super(message, { ...context, field, model });
|
|
5069
|
-
this.field = field;
|
|
5070
|
-
this.model = model;
|
|
5071
|
-
}
|
|
5072
|
-
type = "model_config_error";
|
|
5073
|
-
severity = "warning";
|
|
5074
|
-
category = "validation";
|
|
5075
|
-
};
|
|
5076
4879
|
function validateModelConfig(config2) {
|
|
5077
4880
|
const model = config2.model;
|
|
5078
4881
|
if (!model) {
|
|
@@ -5094,79 +4897,32 @@ function validateModelConfig(config2) {
|
|
|
5094
4897
|
}
|
|
5095
4898
|
}
|
|
5096
4899
|
|
|
5097
|
-
// ../core/src/execution/engine/
|
|
5098
|
-
var
|
|
5099
|
-
|
|
5100
|
-
|
|
5101
|
-
|
|
5102
|
-
|
|
5103
|
-
|
|
5104
|
-
|
|
5105
|
-
}
|
|
5106
|
-
|
|
5107
|
-
|
|
5108
|
-
|
|
5109
|
-
|
|
5110
|
-
constructor(message, context) {
|
|
5111
|
-
super(message, context);
|
|
5112
|
-
}
|
|
5113
|
-
};
|
|
5114
|
-
var AgentCompletionError = class extends ExecutionError {
|
|
5115
|
-
type = "agent_completion_error";
|
|
5116
|
-
severity = "warning";
|
|
5117
|
-
category = "agent";
|
|
5118
|
-
constructor(message, context) {
|
|
5119
|
-
super(message, context);
|
|
5120
|
-
}
|
|
5121
|
-
};
|
|
5122
|
-
var AgentOutputValidationError = class extends ExecutionError {
|
|
5123
|
-
type = "agent_output_validation_error";
|
|
5124
|
-
severity = "info";
|
|
5125
|
-
category = "validation";
|
|
5126
|
-
constructor(message, context) {
|
|
5127
|
-
super(message, context);
|
|
5128
|
-
}
|
|
5129
|
-
};
|
|
5130
|
-
var AgentMaxIterationsError = class extends ExecutionError {
|
|
5131
|
-
type = "agent_max_iterations_error";
|
|
5132
|
-
severity = "critical";
|
|
5133
|
-
category = "agent";
|
|
5134
|
-
constructor(message, context) {
|
|
5135
|
-
super(message, context);
|
|
5136
|
-
}
|
|
5137
|
-
};
|
|
5138
|
-
var AgentTimeoutError = class extends ExecutionError {
|
|
5139
|
-
type = "agent_timeout_error";
|
|
5140
|
-
severity = "critical";
|
|
5141
|
-
category = "agent";
|
|
5142
|
-
constructor(message, context) {
|
|
5143
|
-
super(message, context);
|
|
5144
|
-
}
|
|
5145
|
-
};
|
|
5146
|
-
var AgentCancellationError = class extends ExecutionError {
|
|
5147
|
-
type = "agent_cancellation_error";
|
|
5148
|
-
severity = "warning";
|
|
5149
|
-
category = "agent";
|
|
5150
|
-
constructor(message, context) {
|
|
5151
|
-
super(message, context);
|
|
5152
|
-
}
|
|
5153
|
-
};
|
|
5154
|
-
var AgentStalledError = class extends ExecutionError {
|
|
5155
|
-
type = "agent_stalled_error";
|
|
5156
|
-
severity = "critical";
|
|
5157
|
-
category = "agent";
|
|
5158
|
-
constructor(message, context) {
|
|
5159
|
-
super(message, context);
|
|
4900
|
+
// ../core/src/execution/engine/llm/token-validation.ts
|
|
4901
|
+
var UNKNOWN_MODEL_MIN_TOKENS = 2e3;
|
|
4902
|
+
function validateTokenConfiguration(model, maxOutputTokens) {
|
|
4903
|
+
const modelInfo = getModelInfo(model);
|
|
4904
|
+
const configured = maxOutputTokens || 1e3;
|
|
4905
|
+
if (!modelInfo) {
|
|
4906
|
+
if (configured < UNKNOWN_MODEL_MIN_TOKENS) {
|
|
4907
|
+
throw new InsufficientTokensError(
|
|
4908
|
+
`Unknown model '${model}' requires at least 2000 tokens (conservative default), but only ${configured} configured.`,
|
|
4909
|
+
{ model, required: UNKNOWN_MODEL_MIN_TOKENS, configured }
|
|
4910
|
+
);
|
|
4911
|
+
}
|
|
4912
|
+
return;
|
|
5160
4913
|
}
|
|
5161
|
-
|
|
5162
|
-
|
|
5163
|
-
|
|
5164
|
-
|
|
5165
|
-
|
|
5166
|
-
|
|
5167
|
-
|
|
4914
|
+
if (configured < modelInfo.minTokens) {
|
|
4915
|
+
throw new InsufficientTokensError(
|
|
4916
|
+
`Model ${model} requires at least ${modelInfo.minTokens} tokens, but only ${configured} configured. ${modelInfo.category === "reasoning" ? "Reasoning models need more tokens to generate both internal reasoning and output." : ""}`,
|
|
4917
|
+
{
|
|
4918
|
+
model,
|
|
4919
|
+
required: modelInfo.minTokens,
|
|
4920
|
+
recommended: modelInfo.recommendedTokens,
|
|
4921
|
+
configured
|
|
4922
|
+
}
|
|
4923
|
+
);
|
|
5168
4924
|
}
|
|
5169
|
-
}
|
|
4925
|
+
}
|
|
5170
4926
|
|
|
5171
4927
|
// ../core/src/execution/engine/llm/flow-debug.ts
|
|
5172
4928
|
var enabled;
|
|
@@ -5191,47 +4947,41 @@ function preview(text, n2 = 120) {
|
|
|
5191
4947
|
return { len: text.length, head: text.slice(0, n2) };
|
|
5192
4948
|
}
|
|
5193
4949
|
|
|
5194
|
-
// ../core/src/
|
|
5195
|
-
var
|
|
5196
|
-
|
|
5197
|
-
|
|
5198
|
-
|
|
5199
|
-
|
|
5200
|
-
|
|
5201
|
-
|
|
5202
|
-
|
|
5203
|
-
|
|
4950
|
+
// ../core/src/platform/utils/token-counter.ts
|
|
4951
|
+
var CHARS_PER_TOKEN = 3.5;
|
|
4952
|
+
function estimateTokens(text) {
|
|
4953
|
+
const content = typeof text === "string" ? text : JSON.stringify(text);
|
|
4954
|
+
const chars4 = content.length;
|
|
4955
|
+
return Math.ceil(chars4 / CHARS_PER_TOKEN);
|
|
4956
|
+
}
|
|
4957
|
+
function truncationCharBudget(maxTokens, noticeLength = 0) {
|
|
4958
|
+
return Math.max(0, Math.floor(maxTokens * CHARS_PER_TOKEN) - noticeLength);
|
|
4959
|
+
}
|
|
4960
|
+
var UuidSchema = z.string().uuid();
|
|
4961
|
+
var NonEmptyStringSchema = z.string().trim().min(1).max(1e3);
|
|
4962
|
+
z.enum(["agent", "workflow"]);
|
|
4963
|
+
z.enum(["agent", "workflow", "scheduler", "api"]);
|
|
4964
|
+
z.string().trim().toLowerCase().min(1, "Credential name required").max(100, "Credential name too long (max 100 chars)").regex(
|
|
4965
|
+
/^[a-z0-9]+(-[a-z0-9]+)+$/,
|
|
4966
|
+
"Credential name must be lowercase letters, numbers, and hyphens in format: service-environment (e.g., gmail-prod, attio-dev)"
|
|
4967
|
+
);
|
|
4968
|
+
z.enum(["google-sheets", "google-calendar", "dropbox"]);
|
|
4969
|
+
z.string().min(10, "Authorization code too short").max(1e3, "Authorization code too long");
|
|
4970
|
+
z.string().min(10, "State parameter too short").max(2048, "State parameter too long");
|
|
4971
|
+
z.string().trim().transform((str) => str.replace(/[<>'"]/g, ""));
|
|
4972
|
+
z.string().email();
|
|
4973
|
+
z.string().url();
|
|
4974
|
+
z.object({
|
|
4975
|
+
limit: z.coerce.number().int().min(1).max(100).default(20),
|
|
4976
|
+
offset: z.coerce.number().int().min(0).default(0)
|
|
5204
4977
|
});
|
|
5205
|
-
|
|
5206
|
-
|
|
5207
|
-
|
|
5208
|
-
|
|
5209
|
-
nextActions: z.array(AgentActionSchema)
|
|
4978
|
+
z.string().datetime();
|
|
4979
|
+
z.object({
|
|
4980
|
+
startDate: z.string().datetime(),
|
|
4981
|
+
endDate: z.string().datetime()
|
|
5210
4982
|
});
|
|
5211
|
-
|
|
5212
|
-
|
|
5213
|
-
const configured = maxOutputTokens || 1e3;
|
|
5214
|
-
if (!modelInfo) {
|
|
5215
|
-
if (configured < 2e3) {
|
|
5216
|
-
throw new InsufficientTokensError(
|
|
5217
|
-
`Unknown model '${model}' requires at least 2000 tokens (conservative default), but only ${configured} configured.`,
|
|
5218
|
-
{ model, required: 2e3, configured }
|
|
5219
|
-
);
|
|
5220
|
-
}
|
|
5221
|
-
return;
|
|
5222
|
-
}
|
|
5223
|
-
if (configured < modelInfo.minTokens) {
|
|
5224
|
-
throw new InsufficientTokensError(
|
|
5225
|
-
`Model ${model} requires at least ${modelInfo.minTokens} tokens, but only ${configured} configured. ${modelInfo.category === "reasoning" ? "Reasoning models need more tokens to generate both internal reasoning and output." : ""}`,
|
|
5226
|
-
{
|
|
5227
|
-
model,
|
|
5228
|
-
required: modelInfo.minTokens,
|
|
5229
|
-
recommended: modelInfo.recommendedTokens,
|
|
5230
|
-
configured
|
|
5231
|
-
}
|
|
5232
|
-
);
|
|
5233
|
-
}
|
|
5234
|
-
}
|
|
4983
|
+
|
|
4984
|
+
// ../core/src/execution/engine/agent/reasoning/adapters/messages.ts
|
|
5235
4985
|
function buildUntrustedDataPolicy(securityLevel) {
|
|
5236
4986
|
if (securityLevel === "none") return "";
|
|
5237
4987
|
if (securityLevel === "hardened") {
|
|
@@ -5241,141 +4991,67 @@ function buildUntrustedDataPolicy(securityLevel) {
|
|
|
5241
4991
|
}
|
|
5242
4992
|
function buildAgentMessages(systemPrompt, memory, currentInput, securityLevel, conversationHistory = []) {
|
|
5243
4993
|
const policy = buildUntrustedDataPolicy(securityLevel);
|
|
4994
|
+
const historyMessages = conversationHistory.map(({ role, content }) => ({ role, content }));
|
|
4995
|
+
if (historyMessages.length > 0) {
|
|
4996
|
+
historyMessages[historyMessages.length - 1].cacheBreakpoint = true;
|
|
4997
|
+
}
|
|
5244
4998
|
const messages = [
|
|
5245
4999
|
{ role: "system", content: systemPrompt },
|
|
5246
|
-
...
|
|
5000
|
+
...historyMessages,
|
|
5247
5001
|
{ role: "user", content: policy ? `${policy}
|
|
5248
5002
|
${memory.framing}` : memory.framing },
|
|
5249
|
-
|
|
5003
|
+
// `envelopeWarnings` rides on the envelope message itself so `screenRequest` can use the
|
|
5004
|
+
// verdict already stamped per fragment (`MemoryEntry.warnings`) instead of re-scanning this
|
|
5005
|
+
// string on every iteration it gets rebuilt for (B9 / Wave L6).
|
|
5006
|
+
{
|
|
5007
|
+
role: "user",
|
|
5008
|
+
content: memory.dataEnvelope,
|
|
5009
|
+
...memory.envelopeWarnings !== void 0 && { envelopeWarnings: memory.envelopeWarnings }
|
|
5010
|
+
}
|
|
5250
5011
|
];
|
|
5251
5012
|
if (currentInput) {
|
|
5252
5013
|
messages.push({ role: "user", content: currentInput });
|
|
5253
5014
|
}
|
|
5254
5015
|
return messages;
|
|
5255
5016
|
}
|
|
5256
|
-
|
|
5257
|
-
|
|
5258
|
-
|
|
5259
|
-
|
|
5260
|
-
}
|
|
5261
|
-
|
|
5262
|
-
|
|
5263
|
-
|
|
5264
|
-
|
|
5265
|
-
|
|
5266
|
-
|
|
5267
|
-
|
|
5268
|
-
|
|
5269
|
-
const
|
|
5270
|
-
|
|
5271
|
-
|
|
5272
|
-
|
|
5273
|
-
|
|
5274
|
-
|
|
5275
|
-
|
|
5276
|
-
const responseSchema = buildIterationResponseSchema(
|
|
5277
|
-
request.tools,
|
|
5278
|
-
request.includeMessageAction,
|
|
5279
|
-
request.includeNavigateKnowledge,
|
|
5280
|
-
request.includeMemoryOps
|
|
5281
|
-
);
|
|
5282
|
-
flowLog("agent.iteration.request", {
|
|
5283
|
-
model: request.model,
|
|
5284
|
-
securityLevel: request.securityLevel,
|
|
5285
|
-
maxOutputTokens: request.constraints.maxOutputTokens,
|
|
5286
|
-
toolCount: request.tools.length,
|
|
5287
|
-
includeMessageAction: request.includeMessageAction,
|
|
5288
|
-
includeMemoryOps: request.includeMemoryOps,
|
|
5289
|
-
historyTurns: request.conversationHistory?.length ?? 0,
|
|
5290
|
-
messages: messages.map((m2) => ({ role: m2.role, ...preview(m2.content) }))
|
|
5291
|
-
});
|
|
5292
|
-
const response = await adapter.generate({
|
|
5293
|
-
messages,
|
|
5294
|
-
responseSchema,
|
|
5295
|
-
maxOutputTokens: request.constraints.maxOutputTokens,
|
|
5296
|
-
temperature: request.constraints.temperature,
|
|
5297
|
-
signal: request.signal
|
|
5298
|
-
});
|
|
5299
|
-
try {
|
|
5300
|
-
const validated = AgentIterationOutputSchema.parse(response.output);
|
|
5301
|
-
return {
|
|
5302
|
-
reasoning: validated.reasoning,
|
|
5303
|
-
memoryOps: validated.memoryOps,
|
|
5304
|
-
nextActions: withSynthesizedMessage(validated.nextActions, validated.message)
|
|
5305
|
-
};
|
|
5306
|
-
} catch (error) {
|
|
5307
|
-
flowLog("agent.iteration.validationFailed", {
|
|
5308
|
-
returnedKeys: typeof response.output === "object" && response.output !== null ? Object.keys(response.output) : null,
|
|
5309
|
-
missingRequired: ["reasoning", "nextActions"].filter(
|
|
5310
|
-
(k2) => !(typeof response.output === "object" && response.output !== null && k2 in response.output)
|
|
5311
|
-
),
|
|
5312
|
-
messagePresent: typeof response.output === "object" && response.output !== null && typeof response.output.message === "string",
|
|
5313
|
-
zodIssues: error instanceof ZodError ? error.issues.map((i) => ({ path: i.path.join("."), code: i.code })) : void 0
|
|
5314
|
-
});
|
|
5315
|
-
throw new AgentOutputValidationError("Agent iteration output validation failed", {
|
|
5316
|
-
zodError: error instanceof ZodError ? error.format() : error
|
|
5317
|
-
});
|
|
5318
|
-
}
|
|
5319
|
-
}
|
|
5320
|
-
async function callLLMForAgentCompletion(adapter, request) {
|
|
5321
|
-
validateTokenConfiguration(request.model, request.constraints.maxOutputTokens);
|
|
5322
|
-
const response = await adapter.generate({
|
|
5323
|
-
messages: buildAgentMessages(
|
|
5324
|
-
request.systemPrompt,
|
|
5325
|
-
request.memory,
|
|
5326
|
-
request.currentInput,
|
|
5327
|
-
request.securityLevel,
|
|
5328
|
-
request.conversationHistory
|
|
5329
|
-
),
|
|
5330
|
-
responseSchema: request.outputSchema,
|
|
5331
|
-
// Use output schema directly
|
|
5332
|
-
temperature: request.constraints.temperature || 0.3,
|
|
5333
|
-
maxOutputTokens: request.constraints.maxOutputTokens,
|
|
5334
|
-
signal: request.signal
|
|
5335
|
-
});
|
|
5336
|
-
return response.output;
|
|
5337
|
-
}
|
|
5338
|
-
function cleanJsonSchemaForLLM(schema) {
|
|
5339
|
-
if (!schema || typeof schema !== "object") {
|
|
5340
|
-
return schema;
|
|
5341
|
-
}
|
|
5342
|
-
const cleaned = {};
|
|
5343
|
-
for (const [key, value] of Object.entries(schema)) {
|
|
5344
|
-
if (key === "$schema") {
|
|
5345
|
-
continue;
|
|
5346
|
-
}
|
|
5347
|
-
if (value && typeof value === "object") {
|
|
5348
|
-
if (Array.isArray(value)) {
|
|
5349
|
-
cleaned[key] = value.map((item) => cleanJsonSchemaForLLM(item));
|
|
5350
|
-
} else {
|
|
5351
|
-
cleaned[key] = cleanJsonSchemaForLLM(value);
|
|
5352
|
-
}
|
|
5353
|
-
} else {
|
|
5354
|
-
cleaned[key] = value;
|
|
5355
|
-
}
|
|
5356
|
-
}
|
|
5357
|
-
if (cleaned.type === "object" && cleaned.properties && typeof cleaned.properties === "object" && Object.keys(cleaned.properties).length === 0) {
|
|
5358
|
-
cleaned.properties.noInputRequired = {
|
|
5359
|
-
type: "boolean",
|
|
5360
|
-
description: "No input required for this tool. Pass true or omit entirely."
|
|
5361
|
-
};
|
|
5362
|
-
}
|
|
5363
|
-
return cleaned;
|
|
5017
|
+
|
|
5018
|
+
// ../core/src/execution/engine/agent/reasoning/adapters/response-schema.ts
|
|
5019
|
+
var iterationSchemaCache = /* @__PURE__ */ new WeakMap();
|
|
5020
|
+
function capabilitiesCacheKey(capabilities) {
|
|
5021
|
+
return `${capabilities.message}:${capabilities.memoryOps}`;
|
|
5022
|
+
}
|
|
5023
|
+
function buildIterationResponseSchema(tools, capabilities) {
|
|
5024
|
+
let byCapabilities = iterationSchemaCache.get(tools);
|
|
5025
|
+
if (!byCapabilities) {
|
|
5026
|
+
byCapabilities = /* @__PURE__ */ new Map();
|
|
5027
|
+
iterationSchemaCache.set(tools, byCapabilities);
|
|
5028
|
+
}
|
|
5029
|
+
const cacheKey = capabilitiesCacheKey(capabilities);
|
|
5030
|
+
const cached = byCapabilities.get(cacheKey);
|
|
5031
|
+
if (cached) {
|
|
5032
|
+
return cached;
|
|
5033
|
+
}
|
|
5034
|
+
const schema = buildIterationResponseSchemaUncached(tools, capabilities);
|
|
5035
|
+
byCapabilities.set(cacheKey, schema);
|
|
5036
|
+
return schema;
|
|
5364
5037
|
}
|
|
5365
|
-
function
|
|
5038
|
+
function buildIterationResponseSchemaUncached(tools, capabilities) {
|
|
5366
5039
|
const actionSchemas = [];
|
|
5367
5040
|
for (const tool of tools) {
|
|
5368
5041
|
actionSchemas.push({
|
|
5369
5042
|
type: "object",
|
|
5370
5043
|
properties: {
|
|
5371
5044
|
type: { type: "string", enum: ["tool-call"] },
|
|
5372
|
-
id
|
|
5045
|
+
// No `id`: it used to be required here, forcing the model to mint a unique id on every
|
|
5046
|
+
// tool call of every agent, but nothing downstream ever read it -- not the success path
|
|
5047
|
+
// in `executor.ts`, and the one write on the failure path (`addToolError`'s `toolCallId`)
|
|
5048
|
+
// had zero readers outside tests. Round 3 item B8.
|
|
5373
5049
|
name: { type: "string", enum: [tool.name] },
|
|
5374
5050
|
// Constrain to this specific tool
|
|
5375
|
-
input:
|
|
5376
|
-
//
|
|
5051
|
+
input: tool.inputSchema
|
|
5052
|
+
// Emitted as-is; the per-provider dialect in llm/schema/compile.ts cleans it now
|
|
5377
5053
|
},
|
|
5378
|
-
required: ["type", "
|
|
5054
|
+
required: ["type", "name", "input"],
|
|
5379
5055
|
additionalProperties: false
|
|
5380
5056
|
});
|
|
5381
5057
|
}
|
|
@@ -5387,18 +5063,6 @@ function buildIterationResponseSchema(tools, includeMessageAction, includeNaviga
|
|
|
5387
5063
|
required: ["type"],
|
|
5388
5064
|
additionalProperties: false
|
|
5389
5065
|
});
|
|
5390
|
-
if (includeNavigateKnowledge) {
|
|
5391
|
-
actionSchemas.push({
|
|
5392
|
-
type: "object",
|
|
5393
|
-
properties: {
|
|
5394
|
-
type: { type: "string", enum: ["navigate-knowledge"] },
|
|
5395
|
-
id: { type: "string" },
|
|
5396
|
-
nodeId: { type: "string" }
|
|
5397
|
-
},
|
|
5398
|
-
required: ["type", "id", "nodeId"],
|
|
5399
|
-
additionalProperties: false
|
|
5400
|
-
});
|
|
5401
|
-
}
|
|
5402
5066
|
const properties = {
|
|
5403
5067
|
nextActions: {
|
|
5404
5068
|
type: "array",
|
|
@@ -5407,14 +5071,14 @@ function buildIterationResponseSchema(tools, includeMessageAction, includeNaviga
|
|
|
5407
5071
|
}
|
|
5408
5072
|
}
|
|
5409
5073
|
};
|
|
5410
|
-
if (
|
|
5074
|
+
if (capabilities.message !== "off") {
|
|
5411
5075
|
properties.message = {
|
|
5412
5076
|
type: "string",
|
|
5413
5077
|
description: "Your reply to the user, as plain prose. This is the ONLY field the user sees. Use an empty string only when this iteration just calls tools and you have nothing to say yet."
|
|
5414
5078
|
};
|
|
5415
5079
|
}
|
|
5416
5080
|
properties.reasoning = { type: "string", description: "Your reasoning process" };
|
|
5417
|
-
if (
|
|
5081
|
+
if (capabilities.memoryOps) {
|
|
5418
5082
|
properties.memoryOps = {
|
|
5419
5083
|
type: "object",
|
|
5420
5084
|
properties: {
|
|
@@ -5442,10 +5106,122 @@ function buildIterationResponseSchema(tools, includeMessageAction, includeNaviga
|
|
|
5442
5106
|
};
|
|
5443
5107
|
}
|
|
5444
5108
|
return {
|
|
5445
|
-
type: "object",
|
|
5446
|
-
properties,
|
|
5447
|
-
required:
|
|
5448
|
-
additionalProperties: false
|
|
5109
|
+
type: "object",
|
|
5110
|
+
properties,
|
|
5111
|
+
required: capabilities.message === "required" ? ["nextActions", "message", "reasoning"] : ["nextActions", "reasoning"],
|
|
5112
|
+
additionalProperties: false
|
|
5113
|
+
};
|
|
5114
|
+
}
|
|
5115
|
+
|
|
5116
|
+
// ../core/src/execution/engine/agent/reasoning/adapters/agent-adapter-helpers.ts
|
|
5117
|
+
var MemoryKeyValuePairSchema = z.object({ key: z.string(), value: z.any() });
|
|
5118
|
+
var MemorySetSchema = z.union([z.record(z.string(), z.any()), z.array(MemoryKeyValuePairSchema)]).transform((value) => {
|
|
5119
|
+
if (!Array.isArray(value)) return value;
|
|
5120
|
+
return Object.fromEntries(value.map(({ key, value: v2 }) => [key, v2]));
|
|
5121
|
+
});
|
|
5122
|
+
var MemoryOperationsSchema = z.object({
|
|
5123
|
+
set: MemorySetSchema.optional(),
|
|
5124
|
+
// Accept any value type - framework will stringify
|
|
5125
|
+
delete: z.array(z.string()).optional()
|
|
5126
|
+
});
|
|
5127
|
+
var AgentIterationOutputSchema = z.object({
|
|
5128
|
+
reasoning: z.string(),
|
|
5129
|
+
message: z.string().optional(),
|
|
5130
|
+
memoryOps: MemoryOperationsSchema.optional(),
|
|
5131
|
+
nextActions: z.array(AgentActionSchema)
|
|
5132
|
+
});
|
|
5133
|
+
var REQUIRED_ITERATION_KEYS = Object.entries(AgentIterationOutputSchema.shape).filter(([, fieldSchema]) => !fieldSchema.isOptional()).map(([key]) => key);
|
|
5134
|
+
function withSynthesizedMessage(nextActions, message) {
|
|
5135
|
+
const text = message?.trim();
|
|
5136
|
+
if (!text) {
|
|
5137
|
+
return nextActions;
|
|
5138
|
+
}
|
|
5139
|
+
const alreadyPresent = nextActions.some((action) => action.type === "message" && action.text.trim() === text);
|
|
5140
|
+
if (alreadyPresent) {
|
|
5141
|
+
return nextActions;
|
|
5142
|
+
}
|
|
5143
|
+
return [{ type: "message", text }, ...nextActions];
|
|
5144
|
+
}
|
|
5145
|
+
async function callLLMForAgentIteration(adapter, request) {
|
|
5146
|
+
validateTokenConfiguration(request.model, request.constraints.maxOutputTokens);
|
|
5147
|
+
const messages = buildAgentMessages(
|
|
5148
|
+
request.systemPrompt,
|
|
5149
|
+
request.memory,
|
|
5150
|
+
request.currentInput,
|
|
5151
|
+
request.securityLevel,
|
|
5152
|
+
request.conversationHistory
|
|
5153
|
+
);
|
|
5154
|
+
const responseSchema = buildIterationResponseSchema(request.tools, request.capabilities);
|
|
5155
|
+
flowLog("agent.iteration.request", {
|
|
5156
|
+
model: request.model,
|
|
5157
|
+
securityLevel: request.securityLevel,
|
|
5158
|
+
maxOutputTokens: request.constraints.maxOutputTokens,
|
|
5159
|
+
toolCount: request.tools.length,
|
|
5160
|
+
message: request.capabilities.message,
|
|
5161
|
+
memoryOps: request.capabilities.memoryOps,
|
|
5162
|
+
historyTurns: request.conversationHistory?.length ?? 0,
|
|
5163
|
+
messages: messages.map((m2) => ({ role: m2.role, ...preview(m2.content) }))
|
|
5164
|
+
});
|
|
5165
|
+
let acceptedOutput;
|
|
5166
|
+
const response = await adapter.generate({
|
|
5167
|
+
messages,
|
|
5168
|
+
responseSchema,
|
|
5169
|
+
maxOutputTokens: request.constraints.maxOutputTokens,
|
|
5170
|
+
temperature: request.constraints.temperature,
|
|
5171
|
+
signal: request.signal,
|
|
5172
|
+
accept: (output) => {
|
|
5173
|
+
acceptedOutput = AgentIterationOutputSchema.parse(output);
|
|
5174
|
+
}
|
|
5175
|
+
});
|
|
5176
|
+
try {
|
|
5177
|
+
const validated = acceptedOutput ?? AgentIterationOutputSchema.parse(response.output);
|
|
5178
|
+
return {
|
|
5179
|
+
reasoning: validated.reasoning,
|
|
5180
|
+
memoryOps: validated.memoryOps,
|
|
5181
|
+
nextActions: withSynthesizedMessage(validated.nextActions, validated.message),
|
|
5182
|
+
usage: response.usage,
|
|
5183
|
+
// Same text the real request was billed for -- `estimateTokens`'s bias is a property of the
|
|
5184
|
+
// heuristic itself, not of which text it measures, so this is what calibrates the correction
|
|
5185
|
+
// `MemoryManager` applies to its own (much smaller) slice of the same request.
|
|
5186
|
+
estimatedRequestTokens: estimateTokens(messages.map((m2) => m2.content).join(""))
|
|
5187
|
+
};
|
|
5188
|
+
} catch (error) {
|
|
5189
|
+
flowLog("agent.iteration.validationFailed", {
|
|
5190
|
+
returnedKeys: typeof response.output === "object" && response.output !== null ? Object.keys(response.output) : null,
|
|
5191
|
+
missingRequired: REQUIRED_ITERATION_KEYS.filter(
|
|
5192
|
+
(k2) => !(typeof response.output === "object" && response.output !== null && k2 in response.output)
|
|
5193
|
+
),
|
|
5194
|
+
messagePresent: typeof response.output === "object" && response.output !== null && typeof response.output.message === "string",
|
|
5195
|
+
zodIssues: error instanceof ZodError ? error.issues.map((i) => ({ path: i.path.join("."), code: i.code })) : void 0
|
|
5196
|
+
});
|
|
5197
|
+
throw new LLMResponseParseError("Agent iteration output validation failed", {
|
|
5198
|
+
zodError: error instanceof ZodError ? error.format() : error,
|
|
5199
|
+
returnedKeys: typeof response.output === "object" && response.output !== null ? Object.keys(response.output) : null
|
|
5200
|
+
});
|
|
5201
|
+
}
|
|
5202
|
+
}
|
|
5203
|
+
async function callLLMForAgentCompletion(adapter, request) {
|
|
5204
|
+
validateTokenConfiguration(request.model, request.constraints.maxOutputTokens);
|
|
5205
|
+
const messages = buildAgentMessages(
|
|
5206
|
+
request.systemPrompt,
|
|
5207
|
+
request.memory,
|
|
5208
|
+
request.currentInput,
|
|
5209
|
+
request.securityLevel,
|
|
5210
|
+
request.conversationHistory
|
|
5211
|
+
);
|
|
5212
|
+
const response = await adapter.generate({
|
|
5213
|
+
messages,
|
|
5214
|
+
responseSchema: request.outputSchema,
|
|
5215
|
+
// Use output schema directly
|
|
5216
|
+
// `??`, not `||` -- a falsy-but-legitimate `temperature: 0` was being coerced to 0.3.
|
|
5217
|
+
temperature: request.constraints.temperature ?? 0.3,
|
|
5218
|
+
maxOutputTokens: request.constraints.maxOutputTokens,
|
|
5219
|
+
signal: request.signal
|
|
5220
|
+
});
|
|
5221
|
+
return {
|
|
5222
|
+
output: response.output,
|
|
5223
|
+
usage: response.usage,
|
|
5224
|
+
estimatedRequestTokens: estimateTokens(messages.map((m2) => m2.content).join(""))
|
|
5449
5225
|
};
|
|
5450
5226
|
}
|
|
5451
5227
|
|
|
@@ -5465,7 +5241,7 @@ async function processReasoning(iterationContext) {
|
|
|
5465
5241
|
);
|
|
5466
5242
|
const request = buildReasoningRequest(iterationContext);
|
|
5467
5243
|
const startTime = Date.now();
|
|
5468
|
-
const { reasoning, memoryOps, nextActions } = await callLLMForAgentIteration(adapter, {
|
|
5244
|
+
const { reasoning, memoryOps, nextActions, usage, estimatedRequestTokens } = await callLLMForAgentIteration(adapter, {
|
|
5469
5245
|
systemPrompt: request.systemPrompt,
|
|
5470
5246
|
memory: request.memory,
|
|
5471
5247
|
currentInput: request.currentInput,
|
|
@@ -5474,13 +5250,14 @@ async function processReasoning(iterationContext) {
|
|
|
5474
5250
|
tools: request.tools,
|
|
5475
5251
|
constraints: request.constraints,
|
|
5476
5252
|
model: iterationContext.modelConfig.model,
|
|
5477
|
-
|
|
5478
|
-
includeNavigateKnowledge: request.includeNavigateKnowledge,
|
|
5479
|
-
includeMemoryOps: request.includeMemoryOps,
|
|
5253
|
+
capabilities: request.capabilities,
|
|
5480
5254
|
signal: iterationContext.executionContext.signal
|
|
5481
5255
|
});
|
|
5482
5256
|
const endTime = Date.now();
|
|
5483
5257
|
const duration = endTime - startTime;
|
|
5258
|
+
if (usage?.inputTokens !== void 0 && estimatedRequestTokens !== void 0) {
|
|
5259
|
+
iterationContext.memoryManager.recordActualUsage(estimatedRequestTokens, usage.inputTokens);
|
|
5260
|
+
}
|
|
5484
5261
|
const response = { reasoning, memoryOps, nextActions };
|
|
5485
5262
|
await iterationContext.executionContext.onMessageEvent?.({
|
|
5486
5263
|
type: "agent:reasoning",
|
|
@@ -5529,15 +5306,14 @@ var MEMORY_DOMAINS = {
|
|
|
5529
5306
|
],
|
|
5530
5307
|
/**
|
|
5531
5308
|
* Action-owned keys
|
|
5532
|
-
* Updated by framework actions
|
|
5309
|
+
* Updated by framework actions
|
|
5533
5310
|
* LLM cannot modify these via memoryOps
|
|
5534
5311
|
*
|
|
5535
|
-
* Actions manage framework state that controls execution flow.
|
|
5312
|
+
* Actions manage framework state that controls execution flow. Empty today -- the one
|
|
5313
|
+
* action that ever wrote here was retired. Kept as its own domain because a future
|
|
5314
|
+
* action-managed key belongs here, not folded into TOOL_OWNED.
|
|
5536
5315
|
*/
|
|
5537
|
-
ACTION_OWNED: [
|
|
5538
|
-
"knowledge-map-state"
|
|
5539
|
-
// navigate-knowledge action manages this
|
|
5540
|
-
]
|
|
5316
|
+
ACTION_OWNED: []
|
|
5541
5317
|
/**
|
|
5542
5318
|
* LLM-owned keys
|
|
5543
5319
|
* All keys NOT in TOOL_OWNED or ACTION_OWNED
|
|
@@ -5566,11 +5342,15 @@ function addToolError(memoryManager, action, errorMessage, iteration, turnNumber
|
|
|
5566
5342
|
content: JSON.stringify({
|
|
5567
5343
|
error: errorMessage,
|
|
5568
5344
|
toolName: action.name,
|
|
5569
|
-
toolCallId
|
|
5345
|
+
// No `toolCallId`: it wrote `action.id`, and a repo-wide grep for `toolCallId` found no
|
|
5346
|
+
// reader outside test files -- dead even on the one path that recorded it (B8).
|
|
5570
5347
|
...metadata?.errorType && { errorType: metadata.errorType },
|
|
5571
5348
|
...metadata?.severity && { severity: metadata.severity },
|
|
5572
5349
|
...metadata?.isRetryable !== void 0 && { isRetryable: metadata.isRetryable }
|
|
5573
5350
|
}),
|
|
5351
|
+
// Mirrors the success path in `executeToolCall`. Without it a failed parallel tool call is
|
|
5352
|
+
// attributable only by parsing `content`, which oversized results can truncate into invalid JSON.
|
|
5353
|
+
toolName: action.name,
|
|
5574
5354
|
turnNumber,
|
|
5575
5355
|
iterationNumber: iteration,
|
|
5576
5356
|
// The envelope is ours; `errorMessage` came out of the tool.
|
|
@@ -5652,13 +5432,86 @@ var ToolingError = class extends ExecutionError {
|
|
|
5652
5432
|
function timeoutError(operation) {
|
|
5653
5433
|
return new ToolingError("timeout_error", `Operation timed out: ${operation}`);
|
|
5654
5434
|
}
|
|
5435
|
+
function cancelled(message, details) {
|
|
5436
|
+
return new ToolingError("cancelled", message, details);
|
|
5437
|
+
}
|
|
5655
5438
|
|
|
5656
5439
|
// ../core/src/platform/constants/timeouts.ts
|
|
5657
5440
|
var DEFAULT_TOOL_TIMEOUT = 18e5;
|
|
5441
|
+
var DEFAULT_EXECUTION_TIMEOUT = 72e5;
|
|
5442
|
+
|
|
5443
|
+
// ../core/src/execution/engine/agent/memory/truncation.ts
|
|
5444
|
+
var CLOSING_BRACKET_RESERVE = 32;
|
|
5445
|
+
function stripDanglingTail(text) {
|
|
5446
|
+
let out = text.replace(/,\s*$/, "");
|
|
5447
|
+
const danglingKey = /,?\s*"(?:[^"\\]|\\.)*"\s*:\s*$/;
|
|
5448
|
+
if (danglingKey.test(out)) out = out.replace(danglingKey, "").replace(/,\s*$/, "");
|
|
5449
|
+
return out;
|
|
5450
|
+
}
|
|
5451
|
+
function safeStructuralPrefix(raw, cutAt) {
|
|
5452
|
+
const stack = [];
|
|
5453
|
+
let inString = false;
|
|
5454
|
+
let escaped = false;
|
|
5455
|
+
let openStringStart = -1;
|
|
5456
|
+
const limit = Math.min(cutAt, raw.length);
|
|
5457
|
+
for (let i = 0; i < limit; i++) {
|
|
5458
|
+
const ch = raw[i];
|
|
5459
|
+
if (inString) {
|
|
5460
|
+
if (escaped) escaped = false;
|
|
5461
|
+
else if (ch === "\\") escaped = true;
|
|
5462
|
+
else if (ch === '"') inString = false;
|
|
5463
|
+
continue;
|
|
5464
|
+
}
|
|
5465
|
+
if (ch === '"') {
|
|
5466
|
+
inString = true;
|
|
5467
|
+
openStringStart = i;
|
|
5468
|
+
} else if (ch === "{" || ch === "[") {
|
|
5469
|
+
stack.push(ch === "{" ? "}" : "]");
|
|
5470
|
+
} else if (ch === "}" || ch === "]") {
|
|
5471
|
+
stack.pop();
|
|
5472
|
+
}
|
|
5473
|
+
}
|
|
5474
|
+
const cutPoint = inString ? openStringStart : limit;
|
|
5475
|
+
const base = stripDanglingTail(raw.slice(0, cutPoint));
|
|
5476
|
+
return base + [...stack].reverse().join("");
|
|
5477
|
+
}
|
|
5478
|
+
function truncateContent(content, maxTokens) {
|
|
5479
|
+
const estimated = estimateTokens(content);
|
|
5480
|
+
if (estimated <= maxTokens) return { content };
|
|
5481
|
+
const cutAt = truncationCharBudget(maxTokens, CLOSING_BRACKET_RESERVE);
|
|
5482
|
+
const safeContent = safeStructuralPrefix(content, cutAt);
|
|
5483
|
+
const omittedTokens = estimated - maxTokens;
|
|
5484
|
+
return { content: safeContent, truncated: { omittedTokens } };
|
|
5485
|
+
}
|
|
5658
5486
|
|
|
5659
5487
|
// ../core/src/execution/engine/agent/actions/executor.ts
|
|
5488
|
+
async function emit(iterationContext, event) {
|
|
5489
|
+
const startTime = Date.now();
|
|
5490
|
+
try {
|
|
5491
|
+
await iterationContext.executionContext.onMessageEvent?.(event);
|
|
5492
|
+
} catch (error) {
|
|
5493
|
+
const endTime = Date.now();
|
|
5494
|
+
iterationContext.logger.action(
|
|
5495
|
+
"emit-failed",
|
|
5496
|
+
`onMessageEvent threw for '${event.type}': ${error instanceof Error ? error.message : String(error)}`,
|
|
5497
|
+
iterationContext.iteration,
|
|
5498
|
+
startTime,
|
|
5499
|
+
endTime,
|
|
5500
|
+
endTime - startTime
|
|
5501
|
+
);
|
|
5502
|
+
}
|
|
5503
|
+
}
|
|
5504
|
+
function classifyToolAbort(action, reason) {
|
|
5505
|
+
if (reason === "timeout" || reason instanceof DOMException && reason.name === "TimeoutError") {
|
|
5506
|
+
return timeoutError(action.name);
|
|
5507
|
+
}
|
|
5508
|
+
if (reason === "stalled") {
|
|
5509
|
+
return cancelled(`Tool '${action.name}' cancelled: execution stalled (no heartbeat received)`);
|
|
5510
|
+
}
|
|
5511
|
+
return cancelled(`Tool '${action.name}' cancelled`);
|
|
5512
|
+
}
|
|
5660
5513
|
async function executeToolCall(iterationContext, action) {
|
|
5661
|
-
await iterationContext
|
|
5514
|
+
await emit(iterationContext, {
|
|
5662
5515
|
type: "agent:tool_call",
|
|
5663
5516
|
toolName: action.name,
|
|
5664
5517
|
args: action.input
|
|
@@ -5668,7 +5521,7 @@ async function executeToolCall(iterationContext, action) {
|
|
|
5668
5521
|
if (!tool) {
|
|
5669
5522
|
const toolEndTime = Date.now();
|
|
5670
5523
|
const toolDuration = toolEndTime - toolStartTime;
|
|
5671
|
-
await iterationContext
|
|
5524
|
+
await emit(iterationContext, {
|
|
5672
5525
|
type: "agent:tool_result",
|
|
5673
5526
|
toolName: action.name,
|
|
5674
5527
|
success: false,
|
|
@@ -5711,20 +5564,29 @@ async function executeToolCall(iterationContext, action) {
|
|
|
5711
5564
|
}),
|
|
5712
5565
|
new Promise((_, reject) => {
|
|
5713
5566
|
if (composedSignal.aborted) {
|
|
5714
|
-
reject(
|
|
5567
|
+
reject(classifyToolAbort(action, composedSignal.reason));
|
|
5715
5568
|
return;
|
|
5716
5569
|
}
|
|
5717
|
-
composedSignal.addEventListener("abort", () => reject(
|
|
5570
|
+
composedSignal.addEventListener("abort", () => reject(classifyToolAbort(action, composedSignal.reason)), {
|
|
5571
|
+
once: true
|
|
5572
|
+
});
|
|
5718
5573
|
})
|
|
5719
5574
|
]);
|
|
5720
5575
|
const validatedResult = tool.outputSchema.parse(rawResult);
|
|
5576
|
+
let boundedResult = validatedResult;
|
|
5577
|
+
if (tool.maxOutputTokens !== void 0) {
|
|
5578
|
+
const { content, truncated } = truncateContent(JSON.stringify(validatedResult), tool.maxOutputTokens);
|
|
5579
|
+
if (truncated) {
|
|
5580
|
+
boundedResult = content;
|
|
5581
|
+
}
|
|
5582
|
+
}
|
|
5721
5583
|
const toolEndTime = Date.now();
|
|
5722
5584
|
const toolDuration = toolEndTime - toolStartTime;
|
|
5723
|
-
await iterationContext
|
|
5585
|
+
await emit(iterationContext, {
|
|
5724
5586
|
type: "agent:tool_result",
|
|
5725
5587
|
toolName: action.name,
|
|
5726
5588
|
success: true,
|
|
5727
|
-
result:
|
|
5589
|
+
result: boundedResult
|
|
5728
5590
|
});
|
|
5729
5591
|
iterationContext.logger.toolCall(
|
|
5730
5592
|
action.name,
|
|
@@ -5735,12 +5597,14 @@ async function executeToolCall(iterationContext, action) {
|
|
|
5735
5597
|
true,
|
|
5736
5598
|
void 0,
|
|
5737
5599
|
action.input,
|
|
5738
|
-
|
|
5600
|
+
boundedResult
|
|
5739
5601
|
);
|
|
5740
5602
|
const memoryStartTime = Date.now();
|
|
5603
|
+
const memoryContent = typeof boundedResult === "string" ? boundedResult : JSON.stringify(boundedResult);
|
|
5741
5604
|
iterationContext.memoryManager.addToHistory({
|
|
5742
5605
|
type: "tool-result",
|
|
5743
|
-
content:
|
|
5606
|
+
content: memoryContent,
|
|
5607
|
+
toolName: action.name,
|
|
5744
5608
|
turnNumber: iterationContext.executionContext.sessionTurnNumber ?? null,
|
|
5745
5609
|
iterationNumber: iterationContext.iteration,
|
|
5746
5610
|
source: "tool"
|
|
@@ -5749,7 +5613,7 @@ async function executeToolCall(iterationContext, action) {
|
|
|
5749
5613
|
const memoryDuration = memoryEndTime - memoryStartTime;
|
|
5750
5614
|
iterationContext.logger.action(
|
|
5751
5615
|
"memory-tool-result",
|
|
5752
|
-
`Stored tool-result for ${action.name} (${
|
|
5616
|
+
`Stored tool-result for ${action.name} (${memoryContent.length} chars), history size: ${iterationContext.memoryManager.getHistoryLength()}`,
|
|
5753
5617
|
iterationContext.iteration,
|
|
5754
5618
|
memoryStartTime,
|
|
5755
5619
|
memoryEndTime,
|
|
@@ -5759,7 +5623,7 @@ async function executeToolCall(iterationContext, action) {
|
|
|
5759
5623
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
5760
5624
|
const toolEndTime = Date.now();
|
|
5761
5625
|
const toolDuration = toolEndTime - toolStartTime;
|
|
5762
|
-
await iterationContext
|
|
5626
|
+
await emit(iterationContext, {
|
|
5763
5627
|
type: "agent:tool_result",
|
|
5764
5628
|
toolName: action.name,
|
|
5765
5629
|
success: false,
|
|
@@ -5803,189 +5667,126 @@ async function executeToolCall(iterationContext, action) {
|
|
|
5803
5667
|
}
|
|
5804
5668
|
}
|
|
5805
5669
|
|
|
5806
|
-
// ../core/src/execution/engine/agent/
|
|
5807
|
-
|
|
5808
|
-
|
|
5809
|
-
|
|
5810
|
-
|
|
5811
|
-
|
|
5812
|
-
|
|
5813
|
-
|
|
5814
|
-
|
|
5815
|
-
try {
|
|
5816
|
-
if (!knowledgeMap) {
|
|
5817
|
-
throw new Error("Knowledge map not available - agent does not have knowledge navigation enabled");
|
|
5818
|
-
}
|
|
5819
|
-
const node = knowledgeMap.nodes[action.nodeId];
|
|
5820
|
-
if (!node) {
|
|
5821
|
-
throw new Error(`Knowledge node '${action.nodeId}' not found in knowledge map`);
|
|
5822
|
-
}
|
|
5823
|
-
const content = await node.load(executionContext);
|
|
5824
|
-
node.loaded = true;
|
|
5825
|
-
node.prompt = content.prompt;
|
|
5826
|
-
let childNodesCount = 0;
|
|
5827
|
-
if (content.nodes && Object.keys(content.nodes).length > 0) {
|
|
5828
|
-
for (const [childId, childNode] of Object.entries(content.nodes)) {
|
|
5829
|
-
if (!knowledgeMap.nodes[childId]) {
|
|
5830
|
-
knowledgeMap.nodes[childId] = childNode;
|
|
5831
|
-
childNodesCount++;
|
|
5832
|
-
}
|
|
5833
|
-
}
|
|
5834
|
-
if (childNodesCount > 0) {
|
|
5835
|
-
logger.action(
|
|
5836
|
-
"knowledge-nodes-discovered",
|
|
5837
|
-
`Discovered ${childNodesCount} child nodes from '${action.nodeId}': ${Object.keys(content.nodes).join(", ")}`,
|
|
5838
|
-
iteration,
|
|
5839
|
-
startTime,
|
|
5840
|
-
startTime,
|
|
5841
|
-
0
|
|
5842
|
-
);
|
|
5843
|
-
}
|
|
5844
|
-
}
|
|
5845
|
-
if (content.tools && content.tools.length > 0) {
|
|
5846
|
-
const newTools = [];
|
|
5847
|
-
const skippedTools = [];
|
|
5848
|
-
for (const tool of content.tools) {
|
|
5849
|
-
if (toolRegistry.has(tool.name)) {
|
|
5850
|
-
skippedTools.push(tool.name);
|
|
5851
|
-
} else {
|
|
5852
|
-
toolRegistry.set(tool.name, tool);
|
|
5853
|
-
newTools.push(tool.name);
|
|
5854
|
-
}
|
|
5855
|
-
}
|
|
5856
|
-
if (newTools.length > 0) {
|
|
5857
|
-
logger.action(
|
|
5858
|
-
"knowledge-tools-registered",
|
|
5859
|
-
`Registered ${newTools.length} tools from knowledge node '${action.nodeId}': ${newTools.join(", ")}`,
|
|
5860
|
-
iteration,
|
|
5861
|
-
startTime,
|
|
5862
|
-
startTime,
|
|
5863
|
-
0
|
|
5864
|
-
);
|
|
5865
|
-
}
|
|
5866
|
-
if (skippedTools.length > 0) {
|
|
5867
|
-
logger.action(
|
|
5868
|
-
"knowledge-tools-skipped",
|
|
5869
|
-
`Skipped ${skippedTools.length} already-registered tools: ${skippedTools.join(", ")}`,
|
|
5870
|
-
iteration,
|
|
5871
|
-
startTime,
|
|
5872
|
-
startTime,
|
|
5873
|
-
0
|
|
5874
|
-
);
|
|
5875
|
-
}
|
|
5876
|
-
}
|
|
5877
|
-
const stateKey = "knowledge-map-state";
|
|
5878
|
-
const existingState = memoryManager.get(stateKey);
|
|
5879
|
-
let state;
|
|
5880
|
-
if (existingState) {
|
|
5881
|
-
try {
|
|
5882
|
-
state = JSON.parse(existingState);
|
|
5883
|
-
} catch {
|
|
5884
|
-
state = { loadedNodes: [], version: 1 };
|
|
5885
|
-
}
|
|
5886
|
-
} else {
|
|
5887
|
-
state = { loadedNodes: [], version: 1 };
|
|
5888
|
-
}
|
|
5889
|
-
if (!state.loadedNodes.includes(action.nodeId)) {
|
|
5890
|
-
state.loadedNodes.push(action.nodeId);
|
|
5891
|
-
memoryManager.set(stateKey, JSON.stringify(state));
|
|
5892
|
-
logger.action(
|
|
5893
|
-
"knowledge-state-updated",
|
|
5894
|
-
`Added '${action.nodeId}' to loaded nodes (total: ${state.loadedNodes.length})`,
|
|
5895
|
-
iteration,
|
|
5896
|
-
startTime,
|
|
5897
|
-
startTime,
|
|
5898
|
-
0
|
|
5899
|
-
);
|
|
5900
|
-
}
|
|
5901
|
-
const endTime = Date.now();
|
|
5902
|
-
const duration = endTime - startTime;
|
|
5903
|
-
await executionContext.onMessageEvent?.({
|
|
5904
|
-
type: "agent:tool_result",
|
|
5905
|
-
toolName: "navigate_knowledge",
|
|
5906
|
-
success: true,
|
|
5907
|
-
result: {
|
|
5908
|
-
nodeId: action.nodeId,
|
|
5909
|
-
toolsLoaded: content.tools?.length ?? 0,
|
|
5910
|
-
childNodesDiscovered: childNodesCount,
|
|
5911
|
-
promptLength: content.prompt.length
|
|
5912
|
-
}
|
|
5913
|
-
});
|
|
5914
|
-
logger.toolCall(
|
|
5915
|
-
"navigate_knowledge",
|
|
5916
|
-
iteration,
|
|
5917
|
-
startTime,
|
|
5918
|
-
endTime,
|
|
5919
|
-
duration,
|
|
5920
|
-
true,
|
|
5921
|
-
void 0,
|
|
5922
|
-
{ nodeId: action.nodeId },
|
|
5923
|
-
{
|
|
5924
|
-
nodeId: action.nodeId,
|
|
5925
|
-
toolsLoaded: content.tools?.length ?? 0,
|
|
5926
|
-
childNodesDiscovered: childNodesCount,
|
|
5927
|
-
promptLength: content.prompt.length
|
|
5928
|
-
}
|
|
5929
|
-
);
|
|
5930
|
-
let resultMessage = `Knowledge node '${action.nodeId}' loaded successfully. ${content.tools?.length ?? 0} tools registered.`;
|
|
5931
|
-
if (childNodesCount > 0) {
|
|
5932
|
-
resultMessage += ` ${childNodesCount} child nodes discovered.`;
|
|
5933
|
-
}
|
|
5934
|
-
memoryManager.addToHistory({
|
|
5935
|
-
type: "tool-result",
|
|
5936
|
-
content: resultMessage,
|
|
5937
|
-
turnNumber: executionContext.sessionTurnNumber ?? null,
|
|
5938
|
-
iterationNumber: iteration,
|
|
5939
|
-
// Framework-authored: this string is assembled here from node metadata, not returned by
|
|
5940
|
-
// the node. The node's own prompt text reaches the model through the tool registry.
|
|
5941
|
-
source: "framework"
|
|
5942
|
-
});
|
|
5943
|
-
} catch (error) {
|
|
5944
|
-
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
5945
|
-
const endTime = Date.now();
|
|
5946
|
-
const duration = endTime - startTime;
|
|
5947
|
-
await executionContext.onMessageEvent?.({
|
|
5948
|
-
type: "agent:tool_result",
|
|
5949
|
-
toolName: "navigate_knowledge",
|
|
5950
|
-
success: false,
|
|
5951
|
-
error: errorMessage
|
|
5952
|
-
});
|
|
5953
|
-
logger.toolCall(
|
|
5954
|
-
"navigate_knowledge",
|
|
5955
|
-
iteration,
|
|
5956
|
-
startTime,
|
|
5957
|
-
endTime,
|
|
5958
|
-
duration,
|
|
5959
|
-
false,
|
|
5960
|
-
errorMessage,
|
|
5961
|
-
{ nodeId: action.nodeId },
|
|
5962
|
-
void 0
|
|
5963
|
-
);
|
|
5964
|
-
memoryManager.addToHistory({
|
|
5965
|
-
type: "error",
|
|
5966
|
-
content: `Error loading knowledge node '${action.nodeId}': ${errorMessage}`,
|
|
5967
|
-
turnNumber: executionContext.sessionTurnNumber ?? null,
|
|
5968
|
-
iterationNumber: iteration,
|
|
5969
|
-
// The wrapper text is ours but `errorMessage` is not — a thrown message can carry
|
|
5970
|
-
// third-party content, so this stays outside the trust boundary.
|
|
5971
|
-
source: "tool"
|
|
5972
|
-
});
|
|
5670
|
+
// ../core/src/execution/engine/agent/errors.ts
|
|
5671
|
+
var AgentError = class extends ExecutionError {
|
|
5672
|
+
};
|
|
5673
|
+
var AgentInitializationError = class extends AgentError {
|
|
5674
|
+
type = "agent_initialization_error";
|
|
5675
|
+
severity = "critical";
|
|
5676
|
+
category = "agent";
|
|
5677
|
+
constructor(message, context) {
|
|
5678
|
+
super(message, context);
|
|
5973
5679
|
}
|
|
5974
|
-
|
|
5680
|
+
/** Configuration or credential problems. The next attempt fails identically. */
|
|
5681
|
+
isRetryable() {
|
|
5682
|
+
return false;
|
|
5683
|
+
}
|
|
5684
|
+
};
|
|
5685
|
+
var AgentIterationError = class extends AgentError {
|
|
5686
|
+
type = "agent_iteration_error";
|
|
5687
|
+
severity = "warning";
|
|
5688
|
+
category = "agent";
|
|
5689
|
+
constructor(message, context) {
|
|
5690
|
+
super(message, context);
|
|
5691
|
+
}
|
|
5692
|
+
/** The transient case this class exists for -- a bad tool response or a malformed model turn.
|
|
5693
|
+
* The iteration can be re-driven. This is the verdict that was silently `false` while the class
|
|
5694
|
+
* docstring said "may be retried". */
|
|
5695
|
+
isRetryable() {
|
|
5696
|
+
return true;
|
|
5697
|
+
}
|
|
5698
|
+
};
|
|
5699
|
+
var AgentCompletionError = class extends AgentError {
|
|
5700
|
+
type = "agent_completion_error";
|
|
5701
|
+
severity = "warning";
|
|
5702
|
+
category = "agent";
|
|
5703
|
+
constructor(message, context) {
|
|
5704
|
+
super(message, context);
|
|
5705
|
+
}
|
|
5706
|
+
/** Final-output generation is one LLM call; re-driving it is exactly the retry the docstring describes. */
|
|
5707
|
+
isRetryable() {
|
|
5708
|
+
return true;
|
|
5709
|
+
}
|
|
5710
|
+
};
|
|
5711
|
+
var AgentOutputValidationError = class extends AgentError {
|
|
5712
|
+
type = "agent_output_validation_error";
|
|
5713
|
+
severity = "info";
|
|
5714
|
+
category = "validation";
|
|
5715
|
+
constructor(message, context) {
|
|
5716
|
+
super(message, context);
|
|
5717
|
+
}
|
|
5718
|
+
/** The model produced output that does not match the contract, and the same request produces the same
|
|
5719
|
+
* output. `LLMResponseParseError` is the retryable error for "the model can probably do better next
|
|
5720
|
+
* time"; the reasoning adapter throws that for iteration-response parse failures. */
|
|
5721
|
+
isRetryable() {
|
|
5722
|
+
return false;
|
|
5723
|
+
}
|
|
5724
|
+
};
|
|
5725
|
+
var AgentTimeoutError = class extends AgentError {
|
|
5726
|
+
type = "agent_timeout_error";
|
|
5727
|
+
severity = "critical";
|
|
5728
|
+
category = "agent";
|
|
5729
|
+
constructor(message, context) {
|
|
5730
|
+
super(message, context);
|
|
5731
|
+
}
|
|
5732
|
+
/** The execution ceiling was reached, so a retry has no budget to run in. */
|
|
5733
|
+
isRetryable() {
|
|
5734
|
+
return false;
|
|
5735
|
+
}
|
|
5736
|
+
};
|
|
5737
|
+
var AgentCancellationError = class extends AgentError {
|
|
5738
|
+
type = "agent_cancellation_error";
|
|
5739
|
+
severity = "warning";
|
|
5740
|
+
category = "agent";
|
|
5741
|
+
constructor(message, context) {
|
|
5742
|
+
super(message, context);
|
|
5743
|
+
}
|
|
5744
|
+
/** The user asked for this. Retrying would override an explicit instruction. */
|
|
5745
|
+
isRetryable() {
|
|
5746
|
+
return false;
|
|
5747
|
+
}
|
|
5748
|
+
};
|
|
5749
|
+
var AgentStalledError = class extends AgentError {
|
|
5750
|
+
type = "agent_stalled_error";
|
|
5751
|
+
severity = "critical";
|
|
5752
|
+
category = "agent";
|
|
5753
|
+
constructor(message, context) {
|
|
5754
|
+
super(message, context);
|
|
5755
|
+
}
|
|
5756
|
+
/** Terminalized out of band by the heartbeat monitor; the process that would retry is the one declared dead. */
|
|
5757
|
+
isRetryable() {
|
|
5758
|
+
return false;
|
|
5759
|
+
}
|
|
5760
|
+
};
|
|
5761
|
+
var AgentMemoryValidationError = class extends AgentError {
|
|
5762
|
+
type = "agent_memory_validation_error";
|
|
5763
|
+
severity = "info";
|
|
5764
|
+
category = "validation";
|
|
5765
|
+
constructor(message, context) {
|
|
5766
|
+
super(message, context);
|
|
5767
|
+
}
|
|
5768
|
+
/** A malformed memory entry is a caller bug, not a transient condition. */
|
|
5769
|
+
isRetryable() {
|
|
5770
|
+
return false;
|
|
5771
|
+
}
|
|
5772
|
+
};
|
|
5975
5773
|
|
|
5976
|
-
// ../core/src/execution/engine/agent/actions/
|
|
5977
|
-
|
|
5978
|
-
|
|
5979
|
-
|
|
5980
|
-
|
|
5774
|
+
// ../core/src/execution/engine/agent/actions/errors.ts
|
|
5775
|
+
var AgentNoProgressError = class extends AgentError {
|
|
5776
|
+
type = "agent_no_progress_error";
|
|
5777
|
+
severity = "warning";
|
|
5778
|
+
category = "agent";
|
|
5779
|
+
constructor(message, context) {
|
|
5780
|
+
super(message, context);
|
|
5981
5781
|
}
|
|
5982
|
-
|
|
5983
|
-
|
|
5984
|
-
|
|
5985
|
-
|
|
5986
|
-
}
|
|
5782
|
+
/** Two consecutive empty plans against the same context is not a transient blip -- retrying the
|
|
5783
|
+
* same remaining budget against the same input would plausibly repeat it. */
|
|
5784
|
+
isRetryable() {
|
|
5785
|
+
return false;
|
|
5987
5786
|
}
|
|
5988
|
-
}
|
|
5787
|
+
};
|
|
5788
|
+
|
|
5789
|
+
// ../core/src/execution/engine/agent/actions/processor.ts
|
|
5989
5790
|
function normalizeSessionMessages(actions, sessionCapable) {
|
|
5990
5791
|
if (!sessionCapable) {
|
|
5991
5792
|
return actions;
|
|
@@ -6008,10 +5809,33 @@ function normalizeSessionMessages(actions, sessionCapable) {
|
|
|
6008
5809
|
return [collapsedMessage];
|
|
6009
5810
|
});
|
|
6010
5811
|
}
|
|
5812
|
+
var NO_PROGRESS_STREAK_KEY = "agent.actions.noProgressStreak";
|
|
5813
|
+
var MAX_CONSECUTIVE_NO_PROGRESS_ITERATIONS = 2;
|
|
6011
5814
|
async function processActions(iterationContext, response) {
|
|
6012
|
-
validateActionSequence(response.nextActions);
|
|
6013
5815
|
const normalizedActions = normalizeSessionMessages(response.nextActions, !!iterationContext.config.sessionCapable);
|
|
6014
|
-
|
|
5816
|
+
if (normalizedActions.length === 0) {
|
|
5817
|
+
const previousStreak = iterationContext.executionContext.store.get(NO_PROGRESS_STREAK_KEY) ?? 0;
|
|
5818
|
+
const streak = previousStreak + 1;
|
|
5819
|
+
iterationContext.executionContext.store.set(NO_PROGRESS_STREAK_KEY, streak);
|
|
5820
|
+
iterationContext.memoryManager.addToHistory({
|
|
5821
|
+
type: "error",
|
|
5822
|
+
content: JSON.stringify({
|
|
5823
|
+
error: "No actions were produced this iteration (no tool call, message, or complete). Provide at least one action."
|
|
5824
|
+
}),
|
|
5825
|
+
turnNumber: iterationContext.executionContext.sessionTurnNumber ?? null,
|
|
5826
|
+
iterationNumber: iterationContext.iteration,
|
|
5827
|
+
source: "framework"
|
|
5828
|
+
});
|
|
5829
|
+
if (streak >= MAX_CONSECUTIVE_NO_PROGRESS_ITERATIONS) {
|
|
5830
|
+
throw new AgentNoProgressError(`Agent produced no actions for ${streak} consecutive iterations`, {
|
|
5831
|
+
iteration: iterationContext.iteration,
|
|
5832
|
+
streak
|
|
5833
|
+
});
|
|
5834
|
+
}
|
|
5835
|
+
} else {
|
|
5836
|
+
iterationContext.executionContext.store.delete(NO_PROGRESS_STREAK_KEY);
|
|
5837
|
+
}
|
|
5838
|
+
const completeRequested = normalizedActions.some((action) => action.type === "complete");
|
|
6015
5839
|
const toolCalls = [];
|
|
6016
5840
|
const otherActions = [];
|
|
6017
5841
|
for (const action of normalizedActions) {
|
|
@@ -6021,30 +5845,50 @@ async function processActions(iterationContext, response) {
|
|
|
6021
5845
|
otherActions.push(action);
|
|
6022
5846
|
}
|
|
6023
5847
|
}
|
|
5848
|
+
let shouldComplete = completeRequested && toolCalls.length === 0;
|
|
6024
5849
|
if (toolCalls.length > 0) {
|
|
6025
|
-
await Promise.allSettled(toolCalls.map((action) => executeToolCall(iterationContext, action)));
|
|
5850
|
+
const settled = await Promise.allSettled(toolCalls.map((action) => executeToolCall(iterationContext, action)));
|
|
5851
|
+
settled.forEach((outcome, index2) => {
|
|
5852
|
+
if (outcome.status === "rejected") {
|
|
5853
|
+
const action = toolCalls[index2];
|
|
5854
|
+
const reason = outcome.reason instanceof Error ? outcome.reason.message : String(outcome.reason);
|
|
5855
|
+
iterationContext.logger.action(
|
|
5856
|
+
"tool-call-unhandled-rejection",
|
|
5857
|
+
`executeToolCall rejected outside its own error handling for '${action.name}': ${reason}`,
|
|
5858
|
+
iterationContext.iteration,
|
|
5859
|
+
Date.now(),
|
|
5860
|
+
Date.now(),
|
|
5861
|
+
0
|
|
5862
|
+
);
|
|
5863
|
+
}
|
|
5864
|
+
});
|
|
6026
5865
|
}
|
|
6027
5866
|
for (const action of otherActions) {
|
|
6028
|
-
|
|
6029
|
-
|
|
6030
|
-
|
|
6031
|
-
|
|
6032
|
-
|
|
6033
|
-
shouldComplete = true;
|
|
6034
|
-
break;
|
|
6035
|
-
case "message": {
|
|
6036
|
-
await iterationContext.executionContext.onMessageEvent?.({
|
|
6037
|
-
type: "assistant_message",
|
|
6038
|
-
text: action.text
|
|
6039
|
-
});
|
|
6040
|
-
break;
|
|
6041
|
-
}
|
|
5867
|
+
if (action.type === "message") {
|
|
5868
|
+
await iterationContext.executionContext.onMessageEvent?.({
|
|
5869
|
+
type: "assistant_message",
|
|
5870
|
+
text: action.text
|
|
5871
|
+
});
|
|
6042
5872
|
}
|
|
6043
5873
|
}
|
|
6044
|
-
if (!shouldComplete && iterationContext.config.sessionCapable && toolCalls.length === 0 && normalizedActions.some((a3) => a3.type === "message")
|
|
5874
|
+
if (!shouldComplete && iterationContext.config.sessionCapable && toolCalls.length === 0 && normalizedActions.some((a3) => a3.type === "message")) {
|
|
6045
5875
|
shouldComplete = true;
|
|
6046
5876
|
}
|
|
6047
|
-
|
|
5877
|
+
const completeInferred = shouldComplete && !completeRequested;
|
|
5878
|
+
const stopReason = shouldComplete ? completeRequested ? "complete_requested" : "complete_inferred" : null;
|
|
5879
|
+
flowLog("agent.actions", {
|
|
5880
|
+
iteration: iterationContext.iteration,
|
|
5881
|
+
turnNumber: iterationContext.executionContext.sessionTurnNumber ?? null,
|
|
5882
|
+
actions: normalizedActions.length,
|
|
5883
|
+
types: normalizedActions.map((action) => action.type),
|
|
5884
|
+
toolCalls: toolCalls.map((call2) => call2.name),
|
|
5885
|
+
messages: otherActions.filter((action) => action.type === "message").length,
|
|
5886
|
+
completeRequested,
|
|
5887
|
+
completeInferred,
|
|
5888
|
+
shouldComplete,
|
|
5889
|
+
stopReason
|
|
5890
|
+
});
|
|
5891
|
+
return { shouldComplete, stopReason };
|
|
6048
5892
|
}
|
|
6049
5893
|
|
|
6050
5894
|
// ../core/src/execution/engine/agent/memory/processor.ts
|
|
@@ -6074,41 +5918,109 @@ async function processMemory(memoryManager, response, logger, iteration) {
|
|
|
6074
5918
|
if (deleted) {
|
|
6075
5919
|
logger.action("memory-delete", `Deleted: ${key}`, iteration, startTime, endTime, endTime - startTime);
|
|
6076
5920
|
} else {
|
|
6077
|
-
logger.action(
|
|
5921
|
+
logger.action(
|
|
5922
|
+
"memory-delete-missing",
|
|
5923
|
+
`Attempted to delete non-existent key: ${key}`,
|
|
5924
|
+
iteration,
|
|
5925
|
+
startTime,
|
|
5926
|
+
endTime,
|
|
5927
|
+
endTime - startTime
|
|
5928
|
+
);
|
|
6078
5929
|
}
|
|
6079
5930
|
}
|
|
6080
5931
|
}
|
|
6081
5932
|
}
|
|
6082
5933
|
|
|
6083
|
-
// ../core/src/
|
|
6084
|
-
|
|
6085
|
-
|
|
6086
|
-
|
|
6087
|
-
|
|
5934
|
+
// ../core/src/execution/engine/llm/input-sanitizer.ts
|
|
5935
|
+
var BLOCKING_WARNING_TYPES = [
|
|
5936
|
+
"system_prompt_extraction",
|
|
5937
|
+
"role_manipulation",
|
|
5938
|
+
"delimiter_injection",
|
|
5939
|
+
"tool_injection"
|
|
5940
|
+
];
|
|
5941
|
+
function isBlockingWarningSet(warnings) {
|
|
5942
|
+
const unique = new Set(warnings);
|
|
5943
|
+
return [...unique].filter((warning) => BLOCKING_WARNING_TYPES.includes(warning)).length >= 3;
|
|
5944
|
+
}
|
|
5945
|
+
function sanitizeUserInput(input) {
|
|
5946
|
+
let text;
|
|
5947
|
+
if (typeof input === "string") {
|
|
5948
|
+
text = input;
|
|
5949
|
+
} else if (input && typeof input === "object" && "message" in input) {
|
|
5950
|
+
text = String(input.message);
|
|
5951
|
+
} else if (input === null || input === void 0) {
|
|
5952
|
+
text = "";
|
|
5953
|
+
} else {
|
|
5954
|
+
text = JSON.stringify(input);
|
|
5955
|
+
}
|
|
5956
|
+
const warnings = [];
|
|
5957
|
+
let sanitized = text;
|
|
5958
|
+
const systemPromptPatterns = [
|
|
5959
|
+
/ignore\s+(all\s+)?instructions?/i,
|
|
5960
|
+
/ignore\s+(all\s+)?(previous|prior|above)/i,
|
|
5961
|
+
/disregard\s+(all\s+)?(previous|system)\s+instructions?/i,
|
|
5962
|
+
/print\s+(your\s+)?(system\s+)?prompt/i,
|
|
5963
|
+
/(show|tell)\s+(me\s+)?your\s+(system\s+)?prompt/i,
|
|
5964
|
+
/what\s+(are|is)\s+your\s+(system\s+)?instructions?/i,
|
|
5965
|
+
/show\s+(me\s+)?your\s+configuration/i,
|
|
5966
|
+
/repeat\s+everything\s+before/i
|
|
5967
|
+
];
|
|
5968
|
+
for (const pattern of systemPromptPatterns) {
|
|
5969
|
+
if (pattern.test(text)) {
|
|
5970
|
+
warnings.push("system_prompt_extraction");
|
|
5971
|
+
sanitized = sanitized.replace(pattern, "[REDACTED: system prompt extraction attempt]");
|
|
5972
|
+
break;
|
|
5973
|
+
}
|
|
5974
|
+
}
|
|
5975
|
+
const rolePatterns = [
|
|
5976
|
+
/you\s+are\s+now\s+(a|an|the)/i,
|
|
5977
|
+
/act\s+as\s+(a|an|the)/i,
|
|
5978
|
+
/pretend\s+(you\s+are|to\s+be)/i,
|
|
5979
|
+
/from\s+now\s+on,?\s+you/i,
|
|
5980
|
+
/forget\s+your\s+(previous\s+)?role/i,
|
|
5981
|
+
/jailbreak/i
|
|
5982
|
+
];
|
|
5983
|
+
for (const pattern of rolePatterns) {
|
|
5984
|
+
if (pattern.test(text)) {
|
|
5985
|
+
warnings.push("role_manipulation");
|
|
5986
|
+
sanitized = sanitized.replace(pattern, "[REDACTED: role manipulation attempt]");
|
|
5987
|
+
break;
|
|
5988
|
+
}
|
|
5989
|
+
}
|
|
5990
|
+
const delimiterPatterns = [
|
|
5991
|
+
/^\s*={3,}/m,
|
|
5992
|
+
// === at line start (with optional whitespace)
|
|
5993
|
+
/^\s*-{3,}/m,
|
|
5994
|
+
// --- at line start (with optional whitespace)
|
|
5995
|
+
/^\s*#{2,}\s*SYSTEM/im,
|
|
5996
|
+
// ## SYSTEM headers (with optional whitespace)
|
|
5997
|
+
/<\|?system\|?>/i
|
|
5998
|
+
// <system> or <|system|> tags
|
|
5999
|
+
];
|
|
6000
|
+
for (const pattern of delimiterPatterns) {
|
|
6001
|
+
if (pattern.test(text)) {
|
|
6002
|
+
warnings.push("delimiter_injection");
|
|
6003
|
+
sanitized = sanitized.replace(pattern, "[REDACTED: delimiter injection]");
|
|
6004
|
+
break;
|
|
6005
|
+
}
|
|
6006
|
+
}
|
|
6007
|
+
const toolPatterns = [/<function[>\s]/i, /<tool[>\s]/i, /"type":\s*"tool_call"/i];
|
|
6008
|
+
for (const pattern of toolPatterns) {
|
|
6009
|
+
if (pattern.test(text)) {
|
|
6010
|
+
warnings.push("tool_injection");
|
|
6011
|
+
sanitized = sanitized.replace(pattern, "[REDACTED: tool injection attempt]");
|
|
6012
|
+
break;
|
|
6013
|
+
}
|
|
6014
|
+
}
|
|
6015
|
+
const uniqueWarnings = [...new Set(warnings)];
|
|
6016
|
+
const blocked = isBlockingWarningSet(uniqueWarnings);
|
|
6017
|
+
return {
|
|
6018
|
+
original: input,
|
|
6019
|
+
sanitized,
|
|
6020
|
+
warnings: uniqueWarnings,
|
|
6021
|
+
blocked
|
|
6022
|
+
};
|
|
6088
6023
|
}
|
|
6089
|
-
var UuidSchema = z.string().uuid();
|
|
6090
|
-
var NonEmptyStringSchema = z.string().trim().min(1).max(1e3);
|
|
6091
|
-
z.enum(["agent", "workflow"]);
|
|
6092
|
-
z.enum(["agent", "workflow", "scheduler", "api"]);
|
|
6093
|
-
z.string().trim().toLowerCase().min(1, "Credential name required").max(100, "Credential name too long (max 100 chars)").regex(
|
|
6094
|
-
/^[a-z0-9]+(-[a-z0-9]+)+$/,
|
|
6095
|
-
"Credential name must be lowercase letters, numbers, and hyphens in format: service-environment (e.g., gmail-prod, attio-dev)"
|
|
6096
|
-
);
|
|
6097
|
-
z.enum(["google-sheets", "google-calendar", "dropbox"]);
|
|
6098
|
-
z.string().min(10, "Authorization code too short").max(1e3, "Authorization code too long");
|
|
6099
|
-
z.string().min(10, "State parameter too short").max(2048, "State parameter too long");
|
|
6100
|
-
z.string().trim().transform((str) => str.replace(/[<>'"]/g, ""));
|
|
6101
|
-
z.string().email();
|
|
6102
|
-
z.string().url();
|
|
6103
|
-
z.object({
|
|
6104
|
-
limit: z.coerce.number().int().min(1).max(100).default(20),
|
|
6105
|
-
offset: z.coerce.number().int().min(0).default(0)
|
|
6106
|
-
});
|
|
6107
|
-
z.string().datetime();
|
|
6108
|
-
z.object({
|
|
6109
|
-
startDate: z.string().datetime(),
|
|
6110
|
-
endDate: z.string().datetime()
|
|
6111
|
-
});
|
|
6112
6024
|
|
|
6113
6025
|
// ../core/src/platform/constants/limits.ts
|
|
6114
6026
|
var MAX_SESSION_MEMORY_KEYS = 25;
|
|
@@ -6118,16 +6030,18 @@ var MAX_SINGLE_ENTRY_TOKENS = 2e3;
|
|
|
6118
6030
|
var MAX_TOOL_RESULT_TOKENS = 4e3;
|
|
6119
6031
|
|
|
6120
6032
|
// ../core/src/execution/engine/agent/memory/manager.ts
|
|
6121
|
-
var
|
|
6122
|
-
function
|
|
6123
|
-
const
|
|
6124
|
-
if (
|
|
6125
|
-
|
|
6126
|
-
|
|
6127
|
-
|
|
6128
|
-
|
|
6129
|
-
|
|
6130
|
-
|
|
6033
|
+
var ENVELOPE_FULL_RESULT_WINDOW = 3;
|
|
6034
|
+
function parseIfJson(content) {
|
|
6035
|
+
const trimmed = content.trim();
|
|
6036
|
+
if (!trimmed.startsWith("{") && !trimmed.startsWith("[")) return content;
|
|
6037
|
+
try {
|
|
6038
|
+
return JSON.parse(content);
|
|
6039
|
+
} catch {
|
|
6040
|
+
return content;
|
|
6041
|
+
}
|
|
6042
|
+
}
|
|
6043
|
+
function isInTurnScope(entry, currentTurn) {
|
|
6044
|
+
return !currentTurn || entry.turnNumber === currentTurn || entry.turnNumber == null;
|
|
6131
6045
|
}
|
|
6132
6046
|
function keepAnchored(history, recent) {
|
|
6133
6047
|
if (history.length <= recent + 1) return history;
|
|
@@ -6140,6 +6054,47 @@ var MemoryManager = class {
|
|
|
6140
6054
|
this.logger = logger;
|
|
6141
6055
|
}
|
|
6142
6056
|
cachedSnapshot;
|
|
6057
|
+
/**
|
|
6058
|
+
* Rolling correction for `estimateTokens`'s bias, learned from real provider usage.
|
|
6059
|
+
* `undefined` until the first `recordActualUsage` call -- the cold-start state, where
|
|
6060
|
+
* `estimate()` returns the raw `estimateTokens` output unscaled. See `recordActualUsage`.
|
|
6061
|
+
*/
|
|
6062
|
+
tokenCorrectionFactor;
|
|
6063
|
+
/**
|
|
6064
|
+
* Record how far `estimateTokens` was from reality on a real provider call, and roll it into a
|
|
6065
|
+
* correction applied to every estimate this instance makes from here on -- `getStatus`'s three
|
|
6066
|
+
* token fields and `enforceSessionMemoryTokenLimit`'s eviction check, which is what
|
|
6067
|
+
* `autoCompact`/`enforceHardLimits` actually decide compaction from (C3 / Wave M3).
|
|
6068
|
+
*
|
|
6069
|
+
* `estimateTokens` is `chars / 3.5` -- a constant-ratio guess with no knowledge of JSON escaping,
|
|
6070
|
+
* key overhead, or real tokenizer behaviour. Every provider call already returns an EXACT count
|
|
6071
|
+
* (`usage.inputTokens`) that reaches `ai_calls` and is then dropped; this is where it stops being
|
|
6072
|
+
* dropped, without replacing the estimator outright -- a cold session still needs SOME number
|
|
6073
|
+
* before its first real call completes, so the estimator stays the prior and this only corrects
|
|
6074
|
+
* it once real data exists.
|
|
6075
|
+
*
|
|
6076
|
+
* `estimatedRequestTokens` must be `estimateTokens` applied to the SAME text `actualInputTokens`
|
|
6077
|
+
* was billed for -- the whole assembled request (system prompt, tools, conversation history, the
|
|
6078
|
+
* envelope, everything), not just what this class itself emits. `estimateTokens`'s bias is a
|
|
6079
|
+
* property of the heuristic, not of which slice of the request it is pointed at, so measuring it
|
|
6080
|
+
* against the full request (visible to the caller, not to this class) and applying the result to
|
|
6081
|
+
* this class's own estimates (which can only ever see its own slice) is a fair trade -- one ratio,
|
|
6082
|
+
* calibrated on real data, standing in for a per-segment breakdown nothing needs.
|
|
6083
|
+
*
|
|
6084
|
+
* Exponential moving average, not a straight replace: a single call's ratio is noisy, and a
|
|
6085
|
+
* straight replace lets one outlier swing every compaction decision made afterward. Each new
|
|
6086
|
+
* observation gets 30% weight, converging within a handful of calls without chasing one spike.
|
|
6087
|
+
*/
|
|
6088
|
+
recordActualUsage(estimatedRequestTokens, actualInputTokens) {
|
|
6089
|
+
if (estimatedRequestTokens <= 0) return;
|
|
6090
|
+
const observedRatio = actualInputTokens / estimatedRequestTokens;
|
|
6091
|
+
this.tokenCorrectionFactor = this.tokenCorrectionFactor === void 0 ? observedRatio : this.tokenCorrectionFactor * 0.7 + observedRatio * 0.3;
|
|
6092
|
+
}
|
|
6093
|
+
/** `estimateTokens`, scaled by the learned correction once one exists. See `recordActualUsage`. */
|
|
6094
|
+
estimate(text) {
|
|
6095
|
+
const raw = estimateTokens(text);
|
|
6096
|
+
return this.tokenCorrectionFactor === void 0 ? raw : Math.ceil(raw * this.tokenCorrectionFactor);
|
|
6097
|
+
}
|
|
6143
6098
|
// === Agent Operations (Ultra-Simple) ===
|
|
6144
6099
|
/**
|
|
6145
6100
|
* Set session memory entry (agent provides string, framework wraps it)
|
|
@@ -6148,6 +6103,7 @@ var MemoryManager = class {
|
|
|
6148
6103
|
*/
|
|
6149
6104
|
set(key, content, source = "model") {
|
|
6150
6105
|
const entryTokens = estimateTokens(content);
|
|
6106
|
+
let truncated;
|
|
6151
6107
|
if (entryTokens > MAX_SINGLE_ENTRY_TOKENS) {
|
|
6152
6108
|
const truncateTime = Date.now();
|
|
6153
6109
|
this.logger?.action(
|
|
@@ -6158,9 +6114,9 @@ var MemoryManager = class {
|
|
|
6158
6114
|
truncateTime,
|
|
6159
6115
|
0
|
|
6160
6116
|
);
|
|
6161
|
-
const
|
|
6162
|
-
|
|
6163
|
-
|
|
6117
|
+
const result = truncateContent(content, MAX_SINGLE_ENTRY_TOKENS);
|
|
6118
|
+
content = result.content;
|
|
6119
|
+
truncated = result.truncated;
|
|
6164
6120
|
}
|
|
6165
6121
|
this.memory.sessionMemory[key] = {
|
|
6166
6122
|
type: "context",
|
|
@@ -6170,7 +6126,11 @@ var MemoryManager = class {
|
|
|
6170
6126
|
// Session memory entries are not turn-specific
|
|
6171
6127
|
iterationNumber: null,
|
|
6172
6128
|
// Session memory entries are not iteration-specific
|
|
6173
|
-
source
|
|
6129
|
+
source,
|
|
6130
|
+
...truncated && { truncated },
|
|
6131
|
+
// Screened once, here, instead of by re-scanning the whole envelope on every iteration this
|
|
6132
|
+
// key gets re-sent for — see `MemoryEntry.warnings`.
|
|
6133
|
+
warnings: sanitizeUserInput(content).warnings
|
|
6174
6134
|
};
|
|
6175
6135
|
}
|
|
6176
6136
|
/**
|
|
@@ -6209,14 +6169,17 @@ var MemoryManager = class {
|
|
|
6209
6169
|
});
|
|
6210
6170
|
}
|
|
6211
6171
|
let content = entry.content;
|
|
6212
|
-
|
|
6172
|
+
let truncated;
|
|
6173
|
+
if (entry.type === "tool-result" || entry.type === "error") {
|
|
6213
6174
|
const before = content;
|
|
6214
|
-
|
|
6175
|
+
const result = truncateContent(content, MAX_TOOL_RESULT_TOKENS);
|
|
6176
|
+
content = result.content;
|
|
6177
|
+
truncated = result.truncated;
|
|
6215
6178
|
if (content !== before) {
|
|
6216
6179
|
const truncateTime = Date.now();
|
|
6217
6180
|
this.logger?.action(
|
|
6218
6181
|
"memory-tool-result-truncate",
|
|
6219
|
-
|
|
6182
|
+
`${entry.type === "error" ? "Tool error" : "Tool result"} truncated (${estimateTokens(before)} -> ${MAX_TOOL_RESULT_TOKENS} tokens)`,
|
|
6220
6183
|
entry.iterationNumber ?? 0,
|
|
6221
6184
|
truncateTime,
|
|
6222
6185
|
truncateTime,
|
|
@@ -6227,7 +6190,11 @@ var MemoryManager = class {
|
|
|
6227
6190
|
this.memory.history.push({
|
|
6228
6191
|
...entry,
|
|
6229
6192
|
content,
|
|
6230
|
-
timestamp: Date.now()
|
|
6193
|
+
timestamp: Date.now(),
|
|
6194
|
+
...truncated && { truncated },
|
|
6195
|
+
// Screened once, here, instead of by re-scanning the whole accumulated envelope on every
|
|
6196
|
+
// iteration this entry gets re-sent for — see `MemoryEntry.warnings`.
|
|
6197
|
+
warnings: sanitizeUserInput(content).warnings
|
|
6231
6198
|
});
|
|
6232
6199
|
this.autoCompact();
|
|
6233
6200
|
}
|
|
@@ -6237,7 +6204,7 @@ var MemoryManager = class {
|
|
|
6237
6204
|
*/
|
|
6238
6205
|
autoCompact() {
|
|
6239
6206
|
const status = this.getStatus();
|
|
6240
|
-
if (status.
|
|
6207
|
+
if (status.storedHistoryPercent >= 100) {
|
|
6241
6208
|
const before = this.memory.history.length;
|
|
6242
6209
|
this.memory.history = keepAnchored(this.memory.history, 10);
|
|
6243
6210
|
const compactTime = Date.now();
|
|
@@ -6273,12 +6240,12 @@ var MemoryManager = class {
|
|
|
6273
6240
|
}
|
|
6274
6241
|
this.enforceSessionMemoryTokenLimit();
|
|
6275
6242
|
const status = this.getStatus();
|
|
6276
|
-
if (status.
|
|
6243
|
+
if (status.storedHistoryTokens > status.historyBudget) {
|
|
6277
6244
|
const before = this.memory.history.length;
|
|
6278
6245
|
const emergencyStartTime = Date.now();
|
|
6279
6246
|
this.logger?.action(
|
|
6280
6247
|
"memory-emergency",
|
|
6281
|
-
`History exceeds its token budget (${status.
|
|
6248
|
+
`History exceeds its token budget (${status.storedHistoryTokens}/${status.historyBudget}), forcing emergency compaction`,
|
|
6282
6249
|
0,
|
|
6283
6250
|
emergencyStartTime,
|
|
6284
6251
|
emergencyStartTime,
|
|
@@ -6303,17 +6270,24 @@ var MemoryManager = class {
|
|
|
6303
6270
|
* are not. Eviction is oldest-first by timestamp, matching the key-count path, and always
|
|
6304
6271
|
* leaves at least one entry so a single oversized key degrades to "one key" rather than to
|
|
6305
6272
|
* "memory silently emptied".
|
|
6273
|
+
*
|
|
6274
|
+
* The running total is **recomputed** from the survivors rather than decremented per entry.
|
|
6275
|
+
* `getStatus` estimates the pool as a ceiling of the joined sum, and a per-entry decrement is a
|
|
6276
|
+
* sum of ceilings — the larger of the two by up to one token per key. The running total therefore
|
|
6277
|
+
* fell faster than the pool did, and the loop could exit reporting a fit while the very next
|
|
6278
|
+
* `getStatus` still read over the limit. Recomputing makes the loop's exit condition and the
|
|
6279
|
+
* number it is judged by the same expression. The pool is capped at `MAX_SESSION_MEMORY_KEYS`
|
|
6280
|
+
* entries, so the extra passes are bounded and cheap.
|
|
6306
6281
|
*/
|
|
6307
6282
|
enforceSessionMemoryTokenLimit() {
|
|
6308
6283
|
const { sessionMemoryTokens, sessionMemoryTokenLimit } = this.getStatus();
|
|
6309
6284
|
if (sessionMemoryTokens <= sessionMemoryTokenLimit) return;
|
|
6310
6285
|
const sorted = Object.entries(this.memory.sessionMemory).sort((a3, b2) => a3[1].timestamp - b2[1].timestamp);
|
|
6311
6286
|
const startTime = Date.now();
|
|
6312
|
-
|
|
6287
|
+
const poolTokens = () => this.estimate(sorted.map(([, entry]) => entry.content).join(""));
|
|
6313
6288
|
let dropped = 0;
|
|
6314
|
-
while (
|
|
6315
|
-
|
|
6316
|
-
running -= estimateTokens(evicted.content);
|
|
6289
|
+
while (poolTokens() > sessionMemoryTokenLimit && sorted.length > 1) {
|
|
6290
|
+
sorted.shift();
|
|
6317
6291
|
dropped++;
|
|
6318
6292
|
}
|
|
6319
6293
|
this.memory.sessionMemory = Object.fromEntries(sorted);
|
|
@@ -6336,14 +6310,21 @@ var MemoryManager = class {
|
|
|
6336
6310
|
}
|
|
6337
6311
|
/**
|
|
6338
6312
|
* Get memory status for agent awareness
|
|
6313
|
+
*
|
|
6314
|
+
* @param currentTurn - Turn to scope `historyTokens` / `historyPercent` to. Omit to measure the
|
|
6315
|
+
* whole store, which is what the compaction paths want. Callers building something the model
|
|
6316
|
+
* reads should pass it, so the count describes the set the model is actually handed.
|
|
6339
6317
|
* @returns Memory status with token usage and key counts
|
|
6340
6318
|
*/
|
|
6341
|
-
getStatus() {
|
|
6319
|
+
getStatus(currentTurn) {
|
|
6342
6320
|
const sessionMemoryKeys = Object.keys(this.memory.sessionMemory);
|
|
6343
6321
|
const sessionMemoryContent = Object.values(this.memory.sessionMemory).map((entry) => entry.content).join("");
|
|
6344
|
-
const
|
|
6345
|
-
const
|
|
6346
|
-
const
|
|
6322
|
+
const sessionMemoryTokens = this.estimate(sessionMemoryContent);
|
|
6323
|
+
const storedContent = this.memory.history.map((entry) => entry.content).join("");
|
|
6324
|
+
const storedHistoryTokens = this.estimate(storedContent);
|
|
6325
|
+
const historyTokens = currentTurn === void 0 ? storedHistoryTokens : this.estimate(
|
|
6326
|
+
this.memory.history.filter((entry) => isInTurnScope(entry, currentTurn)).map((entry) => entry.content).join("")
|
|
6327
|
+
);
|
|
6347
6328
|
const tokenBudget = this.constraints.maxMemoryTokens || MAX_MEMORY_TOKENS;
|
|
6348
6329
|
const sessionMemoryTokenLimit = Math.min(MAX_SESSION_MEMORY_TOKENS, Math.floor(tokenBudget * 0.25));
|
|
6349
6330
|
const historyBudget = Math.max(tokenBudget - sessionMemoryTokenLimit, 1);
|
|
@@ -6351,14 +6332,13 @@ var MemoryManager = class {
|
|
|
6351
6332
|
return {
|
|
6352
6333
|
sessionMemoryKeys: sessionMemoryKeys.length,
|
|
6353
6334
|
sessionMemoryLimit,
|
|
6354
|
-
currentKeys: sessionMemoryKeys,
|
|
6355
6335
|
sessionMemoryTokens,
|
|
6356
6336
|
sessionMemoryTokenLimit,
|
|
6357
6337
|
historyPercent: Math.round(historyTokens / historyBudget * 100),
|
|
6358
6338
|
historyTokens,
|
|
6359
|
-
|
|
6360
|
-
|
|
6361
|
-
|
|
6339
|
+
storedHistoryTokens,
|
|
6340
|
+
storedHistoryPercent: Math.round(storedHistoryTokens / historyBudget * 100),
|
|
6341
|
+
historyBudget
|
|
6362
6342
|
};
|
|
6363
6343
|
}
|
|
6364
6344
|
/**
|
|
@@ -6396,48 +6376,62 @@ var MemoryManager = class {
|
|
|
6396
6376
|
* treat "everything in this block" as data was also being handed the live question inside that
|
|
6397
6377
|
* block.
|
|
6398
6378
|
*
|
|
6399
|
-
*
|
|
6379
|
+
* History entries stay chronological. They used to be split into a "current iteration" slot
|
|
6380
|
+
* (reverse chronological, for LLM positional bias) and an "earlier" slot -- but the LLM call
|
|
6381
|
+
* always happens BEFORE `addToHistory` writes that iteration's own entries, so the
|
|
6382
|
+
* current-iteration slot held nothing on any call that mattered. One chronological list replaces
|
|
6383
|
+
* both.
|
|
6384
|
+
*
|
|
6385
|
+
* Tool results (and tool errors) older than `ENVELOPE_FULL_RESULT_WINDOW` iterations are carried
|
|
6386
|
+
* as a short stub instead of their full content -- see `ENVELOPE_FULL_RESULT_WINDOW`. The STORE
|
|
6387
|
+
* (`this.memory.history`) is untouched; only what this call carries is capped.
|
|
6400
6388
|
*
|
|
6401
6389
|
* @param currentIteration - Current iteration number (0 = pre-iteration)
|
|
6402
6390
|
* @param currentTurn - Current turn number (optional, for session context filtering)
|
|
6403
6391
|
*/
|
|
6404
6392
|
toContextParts(currentIteration, currentTurn) {
|
|
6405
|
-
const status = this.getStatus();
|
|
6406
|
-
const inTurnScope = (entry) =>
|
|
6393
|
+
const status = this.getStatus(currentTurn);
|
|
6394
|
+
const inTurnScope = (entry) => isInTurnScope(entry, currentTurn);
|
|
6407
6395
|
const isCurrentInput = (entry) => entry.type === "input" && entry.iterationNumber === 0;
|
|
6408
|
-
const
|
|
6409
|
-
|
|
6410
|
-
(entry) => inTurnScope(entry) && !isCurrentInput(entry) && entry.iterationNumber !== null && entry.iterationNumber < currentIteration
|
|
6396
|
+
const historyEntries = this.memory.history.filter(
|
|
6397
|
+
(entry) => inTurnScope(entry) && !isCurrentInput(entry) && entry.iterationNumber !== null
|
|
6411
6398
|
);
|
|
6412
|
-
const
|
|
6413
|
-
|
|
6414
|
-
|
|
6415
|
-
|
|
6416
|
-
|
|
6417
|
-
|
|
6418
|
-
|
|
6419
|
-
|
|
6420
|
-
|
|
6421
|
-
|
|
6422
|
-
|
|
6423
|
-
|
|
6399
|
+
const isElided = (entry) => (entry.type === "tool-result" || entry.type === "error") && entry.iterationNumber !== null && entry.iterationNumber <= currentIteration - ENVELOPE_FULL_RESULT_WINDOW;
|
|
6400
|
+
const elidedStub = (entry) => `Full ${entry.type === "error" ? "error" : "result"} from ${entry.toolName ?? "this tool call"} elided (iteration ${entry.iterationNumber}, outside the last ${ENVELOPE_FULL_RESULT_WINDOW} iterations carried in full). Re-run the tool if you need this data again.`;
|
|
6401
|
+
const envelopeWarnings = /* @__PURE__ */ new Set();
|
|
6402
|
+
const fragment = (slot, entry, key) => {
|
|
6403
|
+
const elided = isElided(entry);
|
|
6404
|
+
if (!elided) for (const warning of entry.warnings ?? []) envelopeWarnings.add(warning);
|
|
6405
|
+
return {
|
|
6406
|
+
slot,
|
|
6407
|
+
type: entry.type,
|
|
6408
|
+
// `?? 'unknown'` and not a default of 'framework': an unstamped entry predates provenance
|
|
6409
|
+
// or came from a stale bundle, and calling that framework-authored would be a lie in the
|
|
6410
|
+
// one direction that matters. Only carried when it IS 'unknown' -- see `DataEnvelopeFragment`.
|
|
6411
|
+
...(entry.source ?? "unknown") === "unknown" && { source: "unknown" },
|
|
6412
|
+
...entry.toolName !== void 0 && { toolName: entry.toolName },
|
|
6413
|
+
...key !== void 0 && { key },
|
|
6414
|
+
...entry.truncated && { truncated: entry.truncated },
|
|
6415
|
+
content: elided ? elidedStub(entry) : parseIfJson(entry.content)
|
|
6416
|
+
};
|
|
6417
|
+
};
|
|
6424
6418
|
const untrustedData = [
|
|
6425
|
-
...
|
|
6426
|
-
...
|
|
6427
|
-
...earlierContext.map((entry) => fragment("earlier", entry))
|
|
6419
|
+
...historyEntries.map((entry) => fragment("earlier", entry)),
|
|
6420
|
+
...Object.entries(this.memory.sessionMemory).map(([key, entry]) => fragment("session-memory", entry, key))
|
|
6428
6421
|
];
|
|
6422
|
+
const persistNudge = status.storedHistoryPercent >= 80 ? "Memory is filling up -- persist anything you still need now; the framework auto-compacts soon." : "";
|
|
6429
6423
|
const framing = `
|
|
6430
6424
|
=== MEMORY STATUS ===
|
|
6431
|
-
${
|
|
6432
|
-
Session memory: ${status.sessionMemoryTokens}/${status.sessionMemoryTokenLimit} tokens
|
|
6433
|
-
History: ${status.historyTokens}/${status.historyBudget} tokens (${status.historyPercent}% of budget)
|
|
6425
|
+
${persistNudge}
|
|
6434
6426
|
|
|
6435
6427
|
=== HOW TO READ THIS TURN ===
|
|
6436
|
-
The next message lists your stored content under "untrustedData". Each entry records
|
|
6437
|
-
|
|
6438
|
-
|
|
6439
|
-
|
|
6440
|
-
-
|
|
6428
|
+
The next message lists your stored content under "untrustedData". Each entry records which pool it
|
|
6429
|
+
came from ("slot") and what it said ("content"); tool results also carry "toolName" so parallel
|
|
6430
|
+
results stay attributable.
|
|
6431
|
+
- slot "session-memory" persists across turns; "earlier" is this turn's own work, chronological.
|
|
6432
|
+
- a "truncated" field means the stored content was cut to fit a size limit; it names how many
|
|
6433
|
+
tokens were omitted. A tool result naming a tool but no other content means the full result
|
|
6434
|
+
aged out of what gets carried in full -- re-run the tool if you need it again.
|
|
6441
6435
|
${untrustedData.length === 0 ? "Nothing is stored this turn." : `It lists ${untrustedData.length} ${untrustedData.length === 1 ? "fragment" : "fragments"}.`}
|
|
6442
6436
|
The message after it, when present, is this turn's own input.
|
|
6443
6437
|
This is input only. Your own reply is captured as structured output and never looks like this.
|
|
@@ -6455,71 +6449,19 @@ This is input only. Your own reply is captured as structured output and never lo
|
|
|
6455
6449
|
envelopeLen: dataEnvelope.length,
|
|
6456
6450
|
fragments: untrustedData.length,
|
|
6457
6451
|
bySlot: countBy("slot"),
|
|
6458
|
-
bySource: countBy("source"),
|
|
6459
6452
|
sessionMemoryKeys: status.sessionMemoryKeys,
|
|
6460
6453
|
historyTokens: status.historyTokens
|
|
6461
6454
|
});
|
|
6462
|
-
return { framing, dataEnvelope };
|
|
6455
|
+
return { framing, dataEnvelope, envelopeWarnings: [...envelopeWarnings] };
|
|
6463
6456
|
}
|
|
6464
6457
|
};
|
|
6465
|
-
|
|
6466
|
-
// ../core/src/execution/engine/agent/knowledge-map/utils.ts
|
|
6467
|
-
async function reloadKnowledgeMapTools(knowledgeMap, memory, context) {
|
|
6468
|
-
const stateJson = memory.sessionMemory["knowledge-map-state"];
|
|
6469
|
-
if (!stateJson) {
|
|
6470
|
-
return [];
|
|
6471
|
-
}
|
|
6472
|
-
try {
|
|
6473
|
-
const state = JSON.parse(stateJson.content);
|
|
6474
|
-
const tools = [];
|
|
6475
|
-
for (const nodeId of state.loadedNodes) {
|
|
6476
|
-
const node = knowledgeMap.nodes[nodeId];
|
|
6477
|
-
if (!node) {
|
|
6478
|
-
context.logger.warn(`Knowledge node '${nodeId}' not found during reload (skipping)`);
|
|
6479
|
-
continue;
|
|
6480
|
-
}
|
|
6481
|
-
try {
|
|
6482
|
-
const content = await node.load(context);
|
|
6483
|
-
node.loaded = true;
|
|
6484
|
-
node.prompt = content.prompt;
|
|
6485
|
-
if (content.nodes && Object.keys(content.nodes).length > 0) {
|
|
6486
|
-
for (const [childId, childNode] of Object.entries(content.nodes)) {
|
|
6487
|
-
if (!knowledgeMap.nodes[childId]) {
|
|
6488
|
-
knowledgeMap.nodes[childId] = childNode;
|
|
6489
|
-
}
|
|
6490
|
-
}
|
|
6491
|
-
}
|
|
6492
|
-
if (content.tools && content.tools.length > 0) {
|
|
6493
|
-
tools.push(...content.tools);
|
|
6494
|
-
}
|
|
6495
|
-
} catch (error) {
|
|
6496
|
-
const errorMessage = errorToString(error);
|
|
6497
|
-
context.logger.error(`Failed to reload knowledge node '${nodeId}': ${errorMessage}`);
|
|
6498
|
-
}
|
|
6499
|
-
}
|
|
6500
|
-
return tools;
|
|
6501
|
-
} catch (error) {
|
|
6502
|
-
const errorMessage = errorToString(error);
|
|
6503
|
-
context.logger.error(`Failed to parse knowledge-map-state: ${errorMessage}`);
|
|
6504
|
-
return [];
|
|
6505
|
-
}
|
|
6506
|
-
}
|
|
6507
|
-
function initializeKnowledgeMap(knowledgeMap) {
|
|
6508
|
-
if (!knowledgeMap) return void 0;
|
|
6509
|
-
return {
|
|
6510
|
-
nodes: Object.fromEntries(Object.entries(knowledgeMap.nodes).map(([id, node]) => [id, { ...node }]))
|
|
6511
|
-
};
|
|
6512
|
-
}
|
|
6513
|
-
function hasMemoryContent(memory) {
|
|
6514
|
-
return Object.keys(memory.sessionMemory).length > 0 || memory.history.length > 0;
|
|
6515
|
-
}
|
|
6458
|
+
var MAX_ITERATION_PARSE_REDRIVES = 2;
|
|
6516
6459
|
var Agent = class {
|
|
6517
6460
|
// Base properties from definition
|
|
6518
6461
|
config;
|
|
6519
6462
|
contract;
|
|
6520
6463
|
toolRegistry;
|
|
6521
6464
|
modelConfig;
|
|
6522
|
-
knowledgeMap;
|
|
6523
6465
|
definition;
|
|
6524
6466
|
adapterFactory;
|
|
6525
6467
|
initialMemory;
|
|
@@ -6536,6 +6478,16 @@ var Agent = class {
|
|
|
6536
6478
|
* `role:'user'` message, so it is held here rather than re-read from memory history.
|
|
6537
6479
|
*/
|
|
6538
6480
|
currentInput = "";
|
|
6481
|
+
/** How this execution's turn ended -- see `AgentStopReason`. Set once, in `iterate()`. */
|
|
6482
|
+
stopReason = null;
|
|
6483
|
+
/** Consecutive `LLMResponseParseError` count within the CURRENT iteration's re-drives. Reset on
|
|
6484
|
+
* the next iteration that actually produces a valid response -- see `MAX_ITERATION_PARSE_REDRIVES`. */
|
|
6485
|
+
consecutiveParseFailures = 0;
|
|
6486
|
+
/** Whether `assistant_message` fired at least once this turn -- see `hasSpoken()` and the
|
|
6487
|
+
* silence-detector note in `complete()`. Tracked by wrapping `onMessageEvent` rather than by
|
|
6488
|
+
* reading memory history after the fact, because the emit is the user-visible event and memory
|
|
6489
|
+
* can be compacted or restructured without changing whether the turn spoke. */
|
|
6490
|
+
spokeThisTurn = false;
|
|
6539
6491
|
/**
|
|
6540
6492
|
* Create a new agent instance from definition
|
|
6541
6493
|
* Memory will be initialized during execution
|
|
@@ -6551,7 +6503,6 @@ var Agent = class {
|
|
|
6551
6503
|
this.config = definition.config;
|
|
6552
6504
|
this.contract = definition.contract;
|
|
6553
6505
|
this.modelConfig = definition.modelConfig;
|
|
6554
|
-
this.knowledgeMap = initializeKnowledgeMap(definition.knowledgeMap);
|
|
6555
6506
|
this.toolRegistry = /* @__PURE__ */ new Map();
|
|
6556
6507
|
for (const tool of definition.tools) {
|
|
6557
6508
|
this.toolRegistry.set(tool.name, tool);
|
|
@@ -6567,22 +6518,46 @@ var Agent = class {
|
|
|
6567
6518
|
* @returns Validated output matching contract.outputSchema, or null if no output schema
|
|
6568
6519
|
*/
|
|
6569
6520
|
async execute(input, context) {
|
|
6570
|
-
this.executionContext = context;
|
|
6571
|
-
await
|
|
6521
|
+
this.executionContext = this.wrapContextForSilenceDetection(context);
|
|
6522
|
+
await this.executionContext.onMessageEvent?.({ type: "agent:started" });
|
|
6572
6523
|
try {
|
|
6573
|
-
await this.initialize(input,
|
|
6574
|
-
|
|
6524
|
+
await this.initialize(input, this.executionContext);
|
|
6525
|
+
if (this.config.singleShot) {
|
|
6526
|
+
this.stopReason = "single_shot_completed";
|
|
6527
|
+
} else {
|
|
6528
|
+
try {
|
|
6529
|
+
await this.iterate(this.executionContext);
|
|
6530
|
+
} finally {
|
|
6531
|
+
this.memoryManager.toSnapshot();
|
|
6532
|
+
}
|
|
6533
|
+
}
|
|
6575
6534
|
const output = await this.complete();
|
|
6576
|
-
await
|
|
6535
|
+
await this.executionContext.onMessageEvent?.({ type: "agent:completed" });
|
|
6577
6536
|
return output;
|
|
6578
6537
|
} catch (error) {
|
|
6579
|
-
await
|
|
6538
|
+
await this.executionContext.onMessageEvent?.({ type: "agent:error", error: String(error) });
|
|
6580
6539
|
throw error;
|
|
6581
6540
|
}
|
|
6582
6541
|
}
|
|
6583
6542
|
/**
|
|
6584
|
-
*
|
|
6585
|
-
*
|
|
6543
|
+
* Wrap `onMessageEvent` to record whether the turn ever produced an `assistant_message`, without
|
|
6544
|
+
* touching `processActions`/`executor.ts` (which are the actual emitters) -- see `hasSpoken()` and
|
|
6545
|
+
* the silence-detector note in `complete()`. A no-op when the caller supplied no handler: with
|
|
6546
|
+
* nothing listening, there is no event to observe either way.
|
|
6547
|
+
*/
|
|
6548
|
+
wrapContextForSilenceDetection(context) {
|
|
6549
|
+
const emit2 = context.onMessageEvent;
|
|
6550
|
+
if (!emit2) return context;
|
|
6551
|
+
return {
|
|
6552
|
+
...context,
|
|
6553
|
+
onMessageEvent: (event) => {
|
|
6554
|
+
if (event.type === "assistant_message") this.spokeThisTurn = true;
|
|
6555
|
+
return emit2(event);
|
|
6556
|
+
}
|
|
6557
|
+
};
|
|
6558
|
+
}
|
|
6559
|
+
/**
|
|
6560
|
+
* Register additional tools at runtime
|
|
6586
6561
|
*
|
|
6587
6562
|
* @param tools - Array of tools to register
|
|
6588
6563
|
* Note: Silently skips tools that are already registered
|
|
@@ -6617,6 +6592,7 @@ var Agent = class {
|
|
|
6617
6592
|
this.logger.lifecycle("initialization", "started", {
|
|
6618
6593
|
startTime: initStartTime
|
|
6619
6594
|
});
|
|
6595
|
+
this.assertSingleShotEligible();
|
|
6620
6596
|
this.currentInput = JSON.stringify(this.contract.inputSchema.parse(input));
|
|
6621
6597
|
this.memoryManager = await this.initializeMemoryManager(context);
|
|
6622
6598
|
const initEndTime = Date.now();
|
|
@@ -6629,13 +6605,34 @@ var Agent = class {
|
|
|
6629
6605
|
this.wrapAndLogError("initialization", initStartTime, error);
|
|
6630
6606
|
}
|
|
6631
6607
|
}
|
|
6608
|
+
/**
|
|
6609
|
+
* Validates `config.singleShot` (see its doc comment on `AgentConfig`) against the two conditions
|
|
6610
|
+
* the one-call path structurally requires. B6 approved this as an EXPLICIT opt-in, never inferred
|
|
6611
|
+
* from `kind`, `sessionCapable`, or tool count -- so a misconfigured opt-in must fail loudly here
|
|
6612
|
+
* rather than silently falling back to the normal two-call path, which would hide the mistake
|
|
6613
|
+
* instead of surfacing it.
|
|
6614
|
+
*
|
|
6615
|
+
* A no-op when `singleShot` is not set at all -- every existing agent shape is unaffected.
|
|
6616
|
+
*/
|
|
6617
|
+
assertSingleShotEligible() {
|
|
6618
|
+
if (!this.config.singleShot) return;
|
|
6619
|
+
if (this.config.sessionCapable) {
|
|
6620
|
+
throw new AgentInitializationError(
|
|
6621
|
+
`Agent '${this.config.resourceId}' sets singleShot but is also sessionCapable -- singleShot is for non-session agents only (a session turn needs the iteration loop to reply)`,
|
|
6622
|
+
{ agentId: this.config.resourceId, reason: "single_shot_requires_non_session" }
|
|
6623
|
+
);
|
|
6624
|
+
}
|
|
6625
|
+
if (!this.shouldGenerateOutput) {
|
|
6626
|
+
throw new AgentInitializationError(
|
|
6627
|
+
`Agent '${this.config.resourceId}' sets singleShot but declares no contract.outputSchema -- singleShot exists to produce structured output in one call; without an output schema there is nothing for that call to produce`,
|
|
6628
|
+
{ agentId: this.config.resourceId, reason: "single_shot_requires_output_schema" }
|
|
6629
|
+
);
|
|
6630
|
+
}
|
|
6631
|
+
}
|
|
6632
6632
|
/**
|
|
6633
6633
|
* Initialize memory manager with preloaded memory and input entry
|
|
6634
6634
|
* Encapsulates all memory initialization complexity
|
|
6635
6635
|
*
|
|
6636
|
-
* Also handles cross-turn persistence: re-registers tools from knowledge nodes
|
|
6637
|
-
* that were loaded in previous session turns.
|
|
6638
|
-
*
|
|
6639
6636
|
* Reads `this.currentInput`, which `initialize` serializes from the validated input.
|
|
6640
6637
|
*
|
|
6641
6638
|
* @param context - Execution context (passed to preloadMemory)
|
|
@@ -6643,14 +6640,11 @@ var Agent = class {
|
|
|
6643
6640
|
*/
|
|
6644
6641
|
async initializeMemoryManager(context) {
|
|
6645
6642
|
const memory = await this.resolveInitialMemory(context);
|
|
6646
|
-
|
|
6647
|
-
await this.reloadKnowledgeMapTools(memory, context);
|
|
6648
|
-
}
|
|
6643
|
+
const memoryManager = new MemoryManager(memory, this.config.constraints, this.logger);
|
|
6649
6644
|
const inputStartTime = Date.now();
|
|
6650
|
-
|
|
6645
|
+
memoryManager.addToHistory({
|
|
6651
6646
|
type: "input",
|
|
6652
6647
|
content: this.currentInput,
|
|
6653
|
-
timestamp: Date.now(),
|
|
6654
6648
|
turnNumber: context.sessionTurnNumber ?? null,
|
|
6655
6649
|
iterationNumber: 0,
|
|
6656
6650
|
source: "user"
|
|
@@ -6673,7 +6667,7 @@ var Agent = class {
|
|
|
6673
6667
|
sessionMemoryKeys: Object.keys(memory.sessionMemory),
|
|
6674
6668
|
currentInputLen: this.currentInput.length
|
|
6675
6669
|
});
|
|
6676
|
-
return
|
|
6670
|
+
return memoryManager;
|
|
6677
6671
|
}
|
|
6678
6672
|
/**
|
|
6679
6673
|
* Resolve the memory this execution starts from.
|
|
@@ -6713,71 +6707,6 @@ var Agent = class {
|
|
|
6713
6707
|
}
|
|
6714
6708
|
return { sessionMemory: {}, history: [] };
|
|
6715
6709
|
}
|
|
6716
|
-
/**
|
|
6717
|
-
* Reload tools from knowledge map state (cross-turn persistence)
|
|
6718
|
-
*
|
|
6719
|
-
* Reads the knowledge-map-state from sessionMemory and re-registers
|
|
6720
|
-
* tools from previously loaded knowledge nodes.
|
|
6721
|
-
*
|
|
6722
|
-
* @param memory - Agent memory with sessionMemory state
|
|
6723
|
-
* @param context - Execution context
|
|
6724
|
-
*/
|
|
6725
|
-
async reloadKnowledgeMapTools(memory, context) {
|
|
6726
|
-
if (!this.knowledgeMap) {
|
|
6727
|
-
return;
|
|
6728
|
-
}
|
|
6729
|
-
const stateJson = memory.sessionMemory["knowledge-map-state"];
|
|
6730
|
-
if (!stateJson) {
|
|
6731
|
-
return;
|
|
6732
|
-
}
|
|
6733
|
-
const reloadStartTime = Date.now();
|
|
6734
|
-
try {
|
|
6735
|
-
const tools = await reloadKnowledgeMapTools(this.knowledgeMap, memory, context);
|
|
6736
|
-
let registeredCount = 0;
|
|
6737
|
-
let skippedCount = 0;
|
|
6738
|
-
for (const tool of tools) {
|
|
6739
|
-
if (this.toolRegistry.has(tool.name)) {
|
|
6740
|
-
skippedCount++;
|
|
6741
|
-
} else {
|
|
6742
|
-
this.toolRegistry.set(tool.name, tool);
|
|
6743
|
-
registeredCount++;
|
|
6744
|
-
}
|
|
6745
|
-
}
|
|
6746
|
-
const reloadEndTime = Date.now();
|
|
6747
|
-
if (registeredCount > 0) {
|
|
6748
|
-
const state = JSON.parse(stateJson.content);
|
|
6749
|
-
this.logger.action(
|
|
6750
|
-
"knowledge-reload",
|
|
6751
|
-
`Reloaded ${registeredCount} tools from ${state.loadedNodes.length} knowledge nodes: ${state.loadedNodes.join(", ")}`,
|
|
6752
|
-
0,
|
|
6753
|
-
reloadStartTime,
|
|
6754
|
-
reloadEndTime,
|
|
6755
|
-
reloadEndTime - reloadStartTime
|
|
6756
|
-
);
|
|
6757
|
-
}
|
|
6758
|
-
if (skippedCount > 0) {
|
|
6759
|
-
this.logger.action(
|
|
6760
|
-
"knowledge-reload-skipped",
|
|
6761
|
-
`Skipped ${skippedCount} already-registered tools during reload`,
|
|
6762
|
-
0,
|
|
6763
|
-
reloadStartTime,
|
|
6764
|
-
reloadEndTime,
|
|
6765
|
-
reloadEndTime - reloadStartTime
|
|
6766
|
-
);
|
|
6767
|
-
}
|
|
6768
|
-
} catch (error) {
|
|
6769
|
-
const errorMessage = errorToString(error);
|
|
6770
|
-
const reloadEndTime = Date.now();
|
|
6771
|
-
this.logger.action(
|
|
6772
|
-
"knowledge-reload-failed",
|
|
6773
|
-
`Failed to reload knowledge map: ${errorMessage}`,
|
|
6774
|
-
0,
|
|
6775
|
-
reloadStartTime,
|
|
6776
|
-
reloadEndTime,
|
|
6777
|
-
reloadEndTime - reloadStartTime
|
|
6778
|
-
);
|
|
6779
|
-
}
|
|
6780
|
-
}
|
|
6781
6710
|
/**
|
|
6782
6711
|
* Phase 2: Run the agent iteration loop
|
|
6783
6712
|
* Continues until LLM signals completion or max iterations reached
|
|
@@ -6788,32 +6717,53 @@ var Agent = class {
|
|
|
6788
6717
|
const maxIterations = this.config.constraints?.maxIterations || 10;
|
|
6789
6718
|
let iteration = 1;
|
|
6790
6719
|
while (iteration <= maxIterations) {
|
|
6791
|
-
|
|
6792
|
-
|
|
6793
|
-
throw new AgentTimeoutError(`Agent execution exceeded timeout (${this.config.constraints?.timeout}ms)`, {
|
|
6794
|
-
timeout: this.config.constraints?.timeout ?? 0,
|
|
6795
|
-
iteration
|
|
6796
|
-
});
|
|
6797
|
-
}
|
|
6798
|
-
if (context.signal.reason === "stalled") {
|
|
6799
|
-
throw new AgentStalledError("Execution stalled: no heartbeat received within threshold", { iteration });
|
|
6800
|
-
}
|
|
6801
|
-
throw new AgentCancellationError("Execution cancelled by user", { iteration });
|
|
6802
|
-
}
|
|
6720
|
+
const abortError = this.abortErrorFor(context.signal, iteration);
|
|
6721
|
+
if (abortError) throw abortError;
|
|
6803
6722
|
try {
|
|
6804
6723
|
await context.onHeartbeat?.();
|
|
6805
6724
|
} catch {
|
|
6806
6725
|
}
|
|
6807
|
-
|
|
6726
|
+
let result;
|
|
6727
|
+
try {
|
|
6728
|
+
result = await this.runIteration(iteration, context);
|
|
6729
|
+
} catch (error) {
|
|
6730
|
+
if (error instanceof LLMResponseParseError && this.consecutiveParseFailures < MAX_ITERATION_PARSE_REDRIVES) {
|
|
6731
|
+
this.consecutiveParseFailures++;
|
|
6732
|
+
continue;
|
|
6733
|
+
}
|
|
6734
|
+
throw error;
|
|
6735
|
+
}
|
|
6736
|
+
this.consecutiveParseFailures = 0;
|
|
6808
6737
|
if (result.shouldComplete) {
|
|
6738
|
+
this.stopReason = result.stopReason;
|
|
6809
6739
|
return;
|
|
6810
6740
|
}
|
|
6811
6741
|
iteration++;
|
|
6812
6742
|
}
|
|
6813
|
-
|
|
6814
|
-
|
|
6815
|
-
|
|
6816
|
-
|
|
6743
|
+
this.stopReason = "budget_exhausted";
|
|
6744
|
+
}
|
|
6745
|
+
/**
|
|
6746
|
+
* Classify an aborted signal into the typed error the rest of the framework expects, regardless
|
|
6747
|
+
* of where the abort surfaced. An abort landing mid-`fetch` or mid-tool throws whatever the
|
|
6748
|
+
* interrupted operation happens to throw -- a raw `DOMException`, or the bare string `'timeout'`
|
|
6749
|
+
* -- neither of which carries a retry verdict, so `wrapAndLogError` used to fall through to a
|
|
6750
|
+
* plain retryable `AgentIterationError` for both, and a cancelled tool got written to memory as
|
|
6751
|
+
* "tool timed out". Reading `signal.reason` here instead of the caught error is what lets the
|
|
6752
|
+
* between-iteration check (which never has an error, only the signal) and `wrapAndLogError`
|
|
6753
|
+
* (which has both) agree on the same classification.
|
|
6754
|
+
*
|
|
6755
|
+
* @returns `null` when the signal is not aborted -- callers throw only when this returns non-null.
|
|
6756
|
+
*/
|
|
6757
|
+
abortErrorFor(signal, iteration) {
|
|
6758
|
+
if (!signal?.aborted) return null;
|
|
6759
|
+
if (signal.reason === "timeout") {
|
|
6760
|
+
const timeout = this.config.constraints?.timeout ?? DEFAULT_EXECUTION_TIMEOUT;
|
|
6761
|
+
return new AgentTimeoutError(`Agent execution exceeded timeout (${timeout}ms)`, { timeout, iteration });
|
|
6762
|
+
}
|
|
6763
|
+
if (signal.reason === "stalled") {
|
|
6764
|
+
return new AgentStalledError("Execution stalled: no heartbeat received within threshold", { iteration });
|
|
6765
|
+
}
|
|
6766
|
+
return new AgentCancellationError("Execution cancelled by user", { iteration });
|
|
6817
6767
|
}
|
|
6818
6768
|
/**
|
|
6819
6769
|
* Run a single iteration of the agent loop
|
|
@@ -6838,9 +6788,9 @@ var Agent = class {
|
|
|
6838
6788
|
const iterationContext = this.buildIterationContext(iteration, context);
|
|
6839
6789
|
const response = await processReasoning(iterationContext);
|
|
6840
6790
|
await processMemory(this.memoryManager, response, this.logger, iteration);
|
|
6841
|
-
const { shouldComplete } = await processActions(iterationContext, response);
|
|
6791
|
+
const { shouldComplete, stopReason } = await processActions(iterationContext, response);
|
|
6842
6792
|
this.logIterationEnd(iteration, iterationStartTime);
|
|
6843
|
-
return { shouldComplete };
|
|
6793
|
+
return { shouldComplete, stopReason };
|
|
6844
6794
|
} catch (error) {
|
|
6845
6795
|
this.wrapAndLogError("iteration", iterationStartTime, error, { iteration });
|
|
6846
6796
|
}
|
|
@@ -6900,6 +6850,16 @@ var Agent = class {
|
|
|
6900
6850
|
historyEntries: snapshot.history.length
|
|
6901
6851
|
}
|
|
6902
6852
|
});
|
|
6853
|
+
if (this.config.sessionCapable && !this.spokeThisTurn) {
|
|
6854
|
+
this.logger.action(
|
|
6855
|
+
"agent-turn-silent",
|
|
6856
|
+
`Turn ended (stopReason=${this.stopReason ?? "unknown"}) without the agent emitting an assistant message`,
|
|
6857
|
+
this.iterationNumber,
|
|
6858
|
+
completionEndTime,
|
|
6859
|
+
completionEndTime,
|
|
6860
|
+
0
|
|
6861
|
+
);
|
|
6862
|
+
}
|
|
6903
6863
|
return output;
|
|
6904
6864
|
} catch (error) {
|
|
6905
6865
|
this.wrapAndLogError("completion", completionStartTime, error);
|
|
@@ -6927,7 +6887,7 @@ var Agent = class {
|
|
|
6927
6887
|
});
|
|
6928
6888
|
const modelTemperature = this.modelConfig.temperature ?? 0.7;
|
|
6929
6889
|
const initialOutput = await this.callLLMForOutput(
|
|
6930
|
-
this.buildOutputGenerationPrompt(),
|
|
6890
|
+
this.buildOutputGenerationPrompt(outputSchema),
|
|
6931
6891
|
outputSchema,
|
|
6932
6892
|
modelTemperature,
|
|
6933
6893
|
"output-generation"
|
|
@@ -6945,7 +6905,7 @@ var Agent = class {
|
|
|
6945
6905
|
validationTime,
|
|
6946
6906
|
0
|
|
6947
6907
|
);
|
|
6948
|
-
const retryPrompt = this.buildRetryPrompt(initialOutput, initialResult.error);
|
|
6908
|
+
const retryPrompt = this.buildRetryPrompt(outputSchema, initialOutput, initialResult.error);
|
|
6949
6909
|
const retryOutput = await this.callLLMForOutput(
|
|
6950
6910
|
retryPrompt,
|
|
6951
6911
|
outputSchema,
|
|
@@ -6990,7 +6950,8 @@ var Agent = class {
|
|
|
6990
6950
|
},
|
|
6991
6951
|
this.executionContext?.organizationId
|
|
6992
6952
|
);
|
|
6993
|
-
|
|
6953
|
+
this.memoryManager.enforceHardLimits();
|
|
6954
|
+
const completion = await callLLMForAgentCompletion(adapter, {
|
|
6994
6955
|
systemPrompt,
|
|
6995
6956
|
memory: this.memoryManager.toContextParts(this.iterationNumber, this.executionContext?.sessionTurnNumber),
|
|
6996
6957
|
currentInput: this.currentInput,
|
|
@@ -7004,6 +6965,9 @@ var Agent = class {
|
|
|
7004
6965
|
model: this.modelConfig.model,
|
|
7005
6966
|
signal: this.executionContext?.signal
|
|
7006
6967
|
});
|
|
6968
|
+
if (completion.usage && completion.estimatedRequestTokens !== void 0) {
|
|
6969
|
+
this.memoryManager.recordActualUsage(completion.estimatedRequestTokens, completion.usage.inputTokens);
|
|
6970
|
+
}
|
|
7007
6971
|
const generationEndTime = Date.now();
|
|
7008
6972
|
const generationDuration = generationEndTime - generationStartTime;
|
|
7009
6973
|
this.logger.action(
|
|
@@ -7014,7 +6978,7 @@ var Agent = class {
|
|
|
7014
6978
|
generationEndTime,
|
|
7015
6979
|
generationDuration
|
|
7016
6980
|
);
|
|
7017
|
-
return
|
|
6981
|
+
return completion.output;
|
|
7018
6982
|
} catch (error) {
|
|
7019
6983
|
const errorMessage = errorToString(error);
|
|
7020
6984
|
const generationEndTime = Date.now();
|
|
@@ -7035,14 +6999,13 @@ var Agent = class {
|
|
|
7035
6999
|
* Instructs LLM to synthesize execution history into structured output
|
|
7036
7000
|
* Note: Only called from generateFinalOutput() which ensures outputSchema exists
|
|
7037
7001
|
*
|
|
7002
|
+
* @param schemaJson - The output schema, already converted once by the caller. Retrying a
|
|
7003
|
+
* failed attempt calls this a second time for the SAME schema, so the conversion itself is the
|
|
7004
|
+
* caller's job -- `generateFinalOutput` converts `contract.outputSchema` exactly once per
|
|
7005
|
+
* completion call, not once per prompt built from it.
|
|
7038
7006
|
* @returns System prompt for completion phase
|
|
7039
7007
|
*/
|
|
7040
|
-
buildOutputGenerationPrompt() {
|
|
7041
|
-
const schema = this.contract.outputSchema;
|
|
7042
|
-
const schemaJson = zodToJsonSchema(schema, {
|
|
7043
|
-
$refStrategy: "none",
|
|
7044
|
-
errorMessages: true
|
|
7045
|
-
});
|
|
7008
|
+
buildOutputGenerationPrompt(schemaJson) {
|
|
7046
7009
|
return `
|
|
7047
7010
|
You have completed a task. Generate the final output based on the execution history.
|
|
7048
7011
|
|
|
@@ -7069,13 +7032,15 @@ Generate the final output now.
|
|
|
7069
7032
|
/**
|
|
7070
7033
|
* Build retry prompt with validation error context
|
|
7071
7034
|
*
|
|
7035
|
+
* @param schemaJson - The output schema, forwarded to `buildOutputGenerationPrompt` rather than
|
|
7036
|
+
* reconverted here
|
|
7072
7037
|
* @param failedOutput - The output that failed validation
|
|
7073
7038
|
* @param validationError - Zod validation error with details
|
|
7074
7039
|
* @returns System prompt for retry attempt
|
|
7075
7040
|
*/
|
|
7076
|
-
buildRetryPrompt(failedOutput, validationError) {
|
|
7041
|
+
buildRetryPrompt(schemaJson, failedOutput, validationError) {
|
|
7077
7042
|
return `
|
|
7078
|
-
${this.buildOutputGenerationPrompt()}
|
|
7043
|
+
${this.buildOutputGenerationPrompt(schemaJson)}
|
|
7079
7044
|
|
|
7080
7045
|
## Previous Attempt (FAILED VALIDATION)
|
|
7081
7046
|
|
|
@@ -7096,6 +7061,22 @@ Fix the errors and generate a valid output.
|
|
|
7096
7061
|
getMemorySnapshot() {
|
|
7097
7062
|
return this.memoryManager.getSnapshot();
|
|
7098
7063
|
}
|
|
7064
|
+
/**
|
|
7065
|
+
* How the just-finished turn ended -- see `AgentStopReason`. Set once `iterate()` returns,
|
|
7066
|
+
* regardless of which of the three ways it ended; `null` before that (`execute()` has not
|
|
7067
|
+
* reached `iterate()` yet, or it threw before returning).
|
|
7068
|
+
*/
|
|
7069
|
+
getStopReason() {
|
|
7070
|
+
return this.stopReason;
|
|
7071
|
+
}
|
|
7072
|
+
/**
|
|
7073
|
+
* Whether the turn emitted at least one `assistant_message` -- see the silence-detector note in
|
|
7074
|
+
* `complete()`. Always `false` for a non-session agent, which has no `message` action on its
|
|
7075
|
+
* schema at all; that is expected, not a defect.
|
|
7076
|
+
*/
|
|
7077
|
+
hasSpoken() {
|
|
7078
|
+
return this.spokeThisTurn;
|
|
7079
|
+
}
|
|
7099
7080
|
/**
|
|
7100
7081
|
* Build the execution context for the agent
|
|
7101
7082
|
* @param iteration - Current iteration number (1-based)
|
|
@@ -7113,8 +7094,7 @@ Fix the errors and generate a valid output.
|
|
|
7113
7094
|
logger: this.logger,
|
|
7114
7095
|
modelConfig: this.modelConfig,
|
|
7115
7096
|
adapterFactory: this.adapterFactory,
|
|
7116
|
-
currentInput: this.currentInput
|
|
7117
|
-
knowledgeMap: this.knowledgeMap
|
|
7097
|
+
currentInput: this.currentInput
|
|
7118
7098
|
};
|
|
7119
7099
|
}
|
|
7120
7100
|
/**
|
|
@@ -7143,6 +7123,11 @@ Fix the errors and generate a valid output.
|
|
|
7143
7123
|
}
|
|
7144
7124
|
this.logger.lifecycle(phase, "failed", logContext);
|
|
7145
7125
|
}
|
|
7126
|
+
const abortIteration = context?.iteration ?? this.iterationNumber;
|
|
7127
|
+
const abortError = this.abortErrorFor(this.executionContext?.signal, abortIteration);
|
|
7128
|
+
if (abortError) {
|
|
7129
|
+
throw abortError;
|
|
7130
|
+
}
|
|
7146
7131
|
if (error instanceof ExecutionError) {
|
|
7147
7132
|
throw error;
|
|
7148
7133
|
}
|
|
@@ -8451,6 +8436,10 @@ var PostMessageLLMAdapter = class {
|
|
|
8451
8436
|
model: this.model,
|
|
8452
8437
|
messages: request.messages,
|
|
8453
8438
|
responseSchema: request.responseSchema,
|
|
8439
|
+
// Plain data, so unlike `accept` (a function, dropped by this allowlist because it cannot be
|
|
8440
|
+
// structured-cloned) it survives postMessage. The parent-side `case 'llm'` branch in
|
|
8441
|
+
// `tool-dispatcher.ts` puts it back on the LLMGenerateRequest it rebuilds.
|
|
8442
|
+
validationSchema: request.validationSchema,
|
|
8454
8443
|
temperature: request.temperature,
|
|
8455
8444
|
maxOutputTokens: request.maxOutputTokens
|
|
8456
8445
|
}
|
|
@@ -9204,6 +9193,218 @@ z.object({
|
|
|
9204
9193
|
credential: z.string().describe("Credential name registered for this integration")
|
|
9205
9194
|
});
|
|
9206
9195
|
|
|
9196
|
+
// ../core/src/execution/engine/llm/schema/compile.ts
|
|
9197
|
+
function isPlainObject(value) {
|
|
9198
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
9199
|
+
}
|
|
9200
|
+
var UNSUPPORTED_KEYWORDS = /* @__PURE__ */ new Set([
|
|
9201
|
+
"minimum",
|
|
9202
|
+
"maximum",
|
|
9203
|
+
"exclusiveMinimum",
|
|
9204
|
+
"exclusiveMaximum",
|
|
9205
|
+
"multipleOf",
|
|
9206
|
+
"minLength",
|
|
9207
|
+
"maxLength",
|
|
9208
|
+
"pattern",
|
|
9209
|
+
"maxItems",
|
|
9210
|
+
"uniqueItems",
|
|
9211
|
+
"minProperties",
|
|
9212
|
+
"maxProperties",
|
|
9213
|
+
"patternProperties",
|
|
9214
|
+
"propertyNames",
|
|
9215
|
+
"contains",
|
|
9216
|
+
"minContains",
|
|
9217
|
+
"maxContains",
|
|
9218
|
+
"dependentRequired",
|
|
9219
|
+
"dependentSchemas",
|
|
9220
|
+
"if",
|
|
9221
|
+
"then",
|
|
9222
|
+
"else",
|
|
9223
|
+
"not",
|
|
9224
|
+
"$id",
|
|
9225
|
+
"$anchor"
|
|
9226
|
+
]);
|
|
9227
|
+
var SUPPORTED_FORMATS = /* @__PURE__ */ new Set([
|
|
9228
|
+
"date-time",
|
|
9229
|
+
"time",
|
|
9230
|
+
"date",
|
|
9231
|
+
"duration",
|
|
9232
|
+
"email",
|
|
9233
|
+
"hostname",
|
|
9234
|
+
"uri",
|
|
9235
|
+
"ipv4",
|
|
9236
|
+
"ipv6",
|
|
9237
|
+
"uuid"
|
|
9238
|
+
]);
|
|
9239
|
+
var MAX_SCHEMA_DEPTH = 32;
|
|
9240
|
+
function convertNode(node, state, depth) {
|
|
9241
|
+
if (depth > MAX_SCHEMA_DEPTH && state.dialect.strict !== "unsupported") {
|
|
9242
|
+
state.blockers.push("depth>32");
|
|
9243
|
+
return node;
|
|
9244
|
+
}
|
|
9245
|
+
if (Array.isArray(node)) {
|
|
9246
|
+
return node.map((item) => convertNode(item, state, depth + 1));
|
|
9247
|
+
}
|
|
9248
|
+
if (!isPlainObject(node)) {
|
|
9249
|
+
return node;
|
|
9250
|
+
}
|
|
9251
|
+
const strictEngaged = state.dialect.strict !== "unsupported";
|
|
9252
|
+
const out = {};
|
|
9253
|
+
for (const [key, value] of Object.entries(node)) {
|
|
9254
|
+
if (key === "$ref" || key === "$defs" || key === "definitions") {
|
|
9255
|
+
if (state.dialect.refs === "refuse") {
|
|
9256
|
+
state.blockers.push(`unsupported:${key}`);
|
|
9257
|
+
out[key] = value;
|
|
9258
|
+
continue;
|
|
9259
|
+
}
|
|
9260
|
+
out[key] = value;
|
|
9261
|
+
continue;
|
|
9262
|
+
}
|
|
9263
|
+
if (key === "$schema") {
|
|
9264
|
+
if (!state.dialect.allowsSchemaKeyword) {
|
|
9265
|
+
continue;
|
|
9266
|
+
}
|
|
9267
|
+
out.$schema = value;
|
|
9268
|
+
continue;
|
|
9269
|
+
}
|
|
9270
|
+
if (key === "properties") {
|
|
9271
|
+
if (!isPlainObject(value)) {
|
|
9272
|
+
out.properties = value;
|
|
9273
|
+
continue;
|
|
9274
|
+
}
|
|
9275
|
+
const properties = {};
|
|
9276
|
+
for (const [propertyName, propertySchema] of Object.entries(value)) {
|
|
9277
|
+
properties[propertyName] = convertNode(propertySchema, state, depth + 1);
|
|
9278
|
+
}
|
|
9279
|
+
out.properties = properties;
|
|
9280
|
+
continue;
|
|
9281
|
+
}
|
|
9282
|
+
if (key === "items") {
|
|
9283
|
+
out.items = convertNode(value, state, depth + 1);
|
|
9284
|
+
continue;
|
|
9285
|
+
}
|
|
9286
|
+
if (strictEngaged) {
|
|
9287
|
+
if (UNSUPPORTED_KEYWORDS.has(key)) {
|
|
9288
|
+
continue;
|
|
9289
|
+
}
|
|
9290
|
+
if (key === "oneOf") {
|
|
9291
|
+
out.anyOf = convertNode(value, state, depth + 1);
|
|
9292
|
+
continue;
|
|
9293
|
+
}
|
|
9294
|
+
if (key === "format") {
|
|
9295
|
+
if (typeof value === "string" && SUPPORTED_FORMATS.has(value)) {
|
|
9296
|
+
out.format = value;
|
|
9297
|
+
}
|
|
9298
|
+
continue;
|
|
9299
|
+
}
|
|
9300
|
+
if (key === "minItems") {
|
|
9301
|
+
const n2 = typeof value === "number" ? value : 0;
|
|
9302
|
+
out.minItems = n2 > 1 ? 1 : n2;
|
|
9303
|
+
continue;
|
|
9304
|
+
}
|
|
9305
|
+
if (key === "type" && Array.isArray(value)) {
|
|
9306
|
+
out.anyOf = value.map((t) => ({ type: t }));
|
|
9307
|
+
continue;
|
|
9308
|
+
}
|
|
9309
|
+
if (key === "additionalProperties") {
|
|
9310
|
+
continue;
|
|
9311
|
+
}
|
|
9312
|
+
}
|
|
9313
|
+
out[key] = convertNode(value, state, depth + 1);
|
|
9314
|
+
}
|
|
9315
|
+
const isObjectNode = out.type === "object" || isPlainObject(out.properties);
|
|
9316
|
+
if (isObjectNode) {
|
|
9317
|
+
const properties = isPlainObject(out.properties) ? out.properties : void 0;
|
|
9318
|
+
const originalAdditionalProperties = node.additionalProperties;
|
|
9319
|
+
if (strictEngaged) {
|
|
9320
|
+
if (properties) {
|
|
9321
|
+
const declared = Object.keys(properties);
|
|
9322
|
+
const required = Array.isArray(out.required) ? out.required : [];
|
|
9323
|
+
const optional = declared.filter((k2) => !required.includes(k2));
|
|
9324
|
+
if (state.dialect.strict === "allRequired" && optional.length > 0) {
|
|
9325
|
+
state.blockers.push(`optionalProperty:${optional[0]}`);
|
|
9326
|
+
}
|
|
9327
|
+
state.optionalProperties += optional.length;
|
|
9328
|
+
}
|
|
9329
|
+
if (originalAdditionalProperties !== void 0 && originalAdditionalProperties !== false && (!properties || Object.keys(properties).length === 0)) {
|
|
9330
|
+
state.blockers.push("freeFormObject");
|
|
9331
|
+
}
|
|
9332
|
+
out.additionalProperties = false;
|
|
9333
|
+
}
|
|
9334
|
+
if (strictEngaged && (!properties || Object.keys(properties).length === 0)) {
|
|
9335
|
+
out.properties = {};
|
|
9336
|
+
}
|
|
9337
|
+
}
|
|
9338
|
+
return out;
|
|
9339
|
+
}
|
|
9340
|
+
function dedupe(values) {
|
|
9341
|
+
return [...new Set(values)];
|
|
9342
|
+
}
|
|
9343
|
+
function compileSchema(schema, dialect) {
|
|
9344
|
+
if (!isPlainObject(schema)) {
|
|
9345
|
+
const strictEngaged = dialect.strict !== "unsupported";
|
|
9346
|
+
return {
|
|
9347
|
+
schema,
|
|
9348
|
+
sendStrict: false,
|
|
9349
|
+
status: strictEngaged ? "refused" : "notAttempted",
|
|
9350
|
+
refusalReasons: strictEngaged ? ["notAnObject"] : []
|
|
9351
|
+
};
|
|
9352
|
+
}
|
|
9353
|
+
const state = { dialect, blockers: [], optionalProperties: 0 };
|
|
9354
|
+
const compiled = convertNode(schema, state, 0);
|
|
9355
|
+
if (state.blockers.length === 0 && dialect.maxOptionalProperties !== void 0 && state.optionalProperties > dialect.maxOptionalProperties) {
|
|
9356
|
+
state.blockers.push(`optionalPropertyLimit:${state.optionalProperties}>${dialect.maxOptionalProperties}`);
|
|
9357
|
+
}
|
|
9358
|
+
if (dialect.strict === "unsupported") {
|
|
9359
|
+
return {
|
|
9360
|
+
schema: compiled,
|
|
9361
|
+
sendStrict: false,
|
|
9362
|
+
status: "notAttempted",
|
|
9363
|
+
refusalReasons: []
|
|
9364
|
+
};
|
|
9365
|
+
}
|
|
9366
|
+
if (state.blockers.length > 0) {
|
|
9367
|
+
return {
|
|
9368
|
+
schema,
|
|
9369
|
+
sendStrict: false,
|
|
9370
|
+
status: "refused",
|
|
9371
|
+
refusalReasons: dedupe(state.blockers)
|
|
9372
|
+
};
|
|
9373
|
+
}
|
|
9374
|
+
return { schema: compiled, sendStrict: true, status: "applied", refusalReasons: [] };
|
|
9375
|
+
}
|
|
9376
|
+
|
|
9377
|
+
// ../core/src/execution/engine/llm/schema/dialect.ts
|
|
9378
|
+
var ANTHROPIC_DIALECT = {
|
|
9379
|
+
strict: "optionalAllowed",
|
|
9380
|
+
// No placeholder of its own: an empty or absent `properties` becomes a bare `properties: {}`,
|
|
9381
|
+
// synthesized directly by the walk for any dialect engaging strict.
|
|
9382
|
+
// Refuses rather than inlining: detecting recursion through `$defs` is more machinery than the
|
|
9383
|
+
// payoff justifies for a tool-input schema.
|
|
9384
|
+
refs: "refuse",
|
|
9385
|
+
allowsSchemaKeyword: false,
|
|
9386
|
+
// Anthropic's published ceiling. Measured live before this was added: the Command Center
|
|
9387
|
+
// assistant's iteration schema declares 33, so 8 of 8 iterations across a 3-turn session were
|
|
9388
|
+
// 400'd and retried unstrict. `optionalAllowed` is still correct -- an optional property is
|
|
9389
|
+
// representable in this grammar, there is just a cap on how many.
|
|
9390
|
+
maxOptionalProperties: 24
|
|
9391
|
+
};
|
|
9392
|
+
var OPENAI_DIALECT = {
|
|
9393
|
+
strict: "allRequired",
|
|
9394
|
+
refs: "passthrough",
|
|
9395
|
+
allowsSchemaKeyword: false
|
|
9396
|
+
};
|
|
9397
|
+
var OPENROUTER_DIALECT = {
|
|
9398
|
+
strict: "allRequired",
|
|
9399
|
+
refs: "refuse",
|
|
9400
|
+
allowsSchemaKeyword: false
|
|
9401
|
+
};
|
|
9402
|
+
var PROVIDER_DIALECTS = {
|
|
9403
|
+
anthropic: ANTHROPIC_DIALECT,
|
|
9404
|
+
openai: OPENAI_DIALECT,
|
|
9405
|
+
openrouter: OPENROUTER_DIALECT
|
|
9406
|
+
};
|
|
9407
|
+
|
|
9207
9408
|
// ../core/src/business/acquisition/ontology-validation.ts
|
|
9208
9409
|
var LEAD_GEN_API_INTERFACE = SYSTEM_INTERFACE_PROFILES[0];
|
|
9209
9410
|
var CRM_API_INTERFACE = SYSTEM_INTERFACE_PROFILES[1];
|
|
@@ -10080,6 +10281,8 @@ function validateDeploymentSpec(orgName, resources) {
|
|
|
10080
10281
|
}
|
|
10081
10282
|
seenIds.add(id);
|
|
10082
10283
|
validateResourceModelConfig(orgName, id, agent.modelConfig);
|
|
10284
|
+
validateAgentGrammar(orgName, id, agent);
|
|
10285
|
+
validateAgentCheapAssertions(orgName, id, agent);
|
|
10083
10286
|
if (agent.interface) {
|
|
10084
10287
|
validateExecutionInterface(orgName, id, agent.interface, agent.contract.inputSchema);
|
|
10085
10288
|
}
|
|
@@ -10090,6 +10293,7 @@ function validateDeploymentSpec(orgName, resources) {
|
|
|
10090
10293
|
function validateResourceModelConfig(orgName, resourceId, modelConfig) {
|
|
10091
10294
|
try {
|
|
10092
10295
|
validateModelConfig(modelConfig);
|
|
10296
|
+
validateTokenConfiguration(modelConfig.model, modelConfig.maxOutputTokens);
|
|
10093
10297
|
} catch (error) {
|
|
10094
10298
|
if (error instanceof ModelConfigError) {
|
|
10095
10299
|
throw new RegistryValidationError(
|
|
@@ -10099,9 +10303,127 @@ function validateResourceModelConfig(orgName, resourceId, modelConfig) {
|
|
|
10099
10303
|
`Invalid model config in ${orgName}/${resourceId}: ${error.message} (field: ${error.field})`
|
|
10100
10304
|
);
|
|
10101
10305
|
}
|
|
10306
|
+
if (error instanceof InsufficientTokensError) {
|
|
10307
|
+
throw new RegistryValidationError(
|
|
10308
|
+
orgName,
|
|
10309
|
+
resourceId,
|
|
10310
|
+
"modelConfig.maxOutputTokens",
|
|
10311
|
+
`Invalid model config in ${orgName}/${resourceId}: ${error.message} (field: modelConfig.maxOutputTokens)`
|
|
10312
|
+
);
|
|
10313
|
+
}
|
|
10102
10314
|
throw error;
|
|
10103
10315
|
}
|
|
10104
10316
|
}
|
|
10317
|
+
function dialectForProvider(provider) {
|
|
10318
|
+
return provider in PROVIDER_DIALECTS ? PROVIDER_DIALECTS[provider] : void 0;
|
|
10319
|
+
}
|
|
10320
|
+
function isStubDefinition(agent) {
|
|
10321
|
+
return agent.modelConfig?.provider === "mock";
|
|
10322
|
+
}
|
|
10323
|
+
function agentCapabilitiesForGrammarCheck(config2) {
|
|
10324
|
+
return {
|
|
10325
|
+
message: config2.sessionCapable ? config2.messagePolicy ?? "required" : "off",
|
|
10326
|
+
memoryOps: !!config2.memoryPreferences
|
|
10327
|
+
};
|
|
10328
|
+
}
|
|
10329
|
+
function findFreeFormObjectField(schema, path = "") {
|
|
10330
|
+
if (typeof schema !== "object" || schema === null || Array.isArray(schema)) return void 0;
|
|
10331
|
+
const node = schema;
|
|
10332
|
+
const properties = node.properties;
|
|
10333
|
+
const isObjectNode = node.type === "object" || typeof properties === "object" && properties !== null;
|
|
10334
|
+
if (!isObjectNode) return void 0;
|
|
10335
|
+
const propertyEntries = typeof properties === "object" && properties !== null ? Object.entries(properties) : [];
|
|
10336
|
+
if (propertyEntries.length === 0 && node.additionalProperties !== void 0 && node.additionalProperties !== false) {
|
|
10337
|
+
return path || "(root)";
|
|
10338
|
+
}
|
|
10339
|
+
for (const [key, value] of propertyEntries) {
|
|
10340
|
+
const found2 = findFreeFormObjectField(value, path ? `${path}.${key}` : key);
|
|
10341
|
+
if (found2) return found2;
|
|
10342
|
+
}
|
|
10343
|
+
if (typeof node.items === "object" && node.items !== null) {
|
|
10344
|
+
return findFreeFormObjectField(node.items, path ? `${path}[]` : "[]");
|
|
10345
|
+
}
|
|
10346
|
+
return void 0;
|
|
10347
|
+
}
|
|
10348
|
+
function describeGrammarRefusal(reasons, toolInputSchema) {
|
|
10349
|
+
const optionalPropertyReason = reasons.find((r2) => r2.startsWith("optionalProperty:"));
|
|
10350
|
+
if (optionalPropertyReason) {
|
|
10351
|
+
const field = optionalPropertyReason.split(":")[1];
|
|
10352
|
+
return `declares optional property '${field}', which this provider's strict mode requires to be listed in 'required'`;
|
|
10353
|
+
}
|
|
10354
|
+
if (reasons.includes("freeFormObject")) {
|
|
10355
|
+
const field = findFreeFormObjectField(toolInputSchema);
|
|
10356
|
+
return field ? `field '${field}' is a free-form object (e.g. z.record(...)) with no declared shape, which this provider's strict mode cannot represent` : `contains a free-form object field with no declared shape, which this provider's strict mode cannot represent`;
|
|
10357
|
+
}
|
|
10358
|
+
if (reasons.some((r2) => r2.startsWith("unsupported:"))) {
|
|
10359
|
+
return `uses '$ref'/'$defs', which this provider's strict mode refuses`;
|
|
10360
|
+
}
|
|
10361
|
+
if (reasons.includes("depth>32")) {
|
|
10362
|
+
return `nests more than 32 levels deep, past this provider's strict-mode limit`;
|
|
10363
|
+
}
|
|
10364
|
+
return `refused strict mode: ${reasons.join(", ")}`;
|
|
10365
|
+
}
|
|
10366
|
+
function validateAgentGrammar(orgName, agentId, agent) {
|
|
10367
|
+
const dialect = dialectForProvider(agent.modelConfig.provider);
|
|
10368
|
+
if (!dialect) return;
|
|
10369
|
+
const capabilities = agentCapabilitiesForGrammarCheck(agent.config);
|
|
10370
|
+
const toolSchemas = agent.tools.map((tool) => ({
|
|
10371
|
+
tool,
|
|
10372
|
+
inputSchema: zodToJsonSchema(tool.inputSchema, { $refStrategy: "none" })
|
|
10373
|
+
}));
|
|
10374
|
+
const toolDefinitions = toolSchemas.map(({ tool, inputSchema }) => ({
|
|
10375
|
+
name: tool.name,
|
|
10376
|
+
description: tool.description,
|
|
10377
|
+
inputSchema
|
|
10378
|
+
}));
|
|
10379
|
+
const iterationSchema = buildIterationResponseSchema(toolDefinitions, capabilities);
|
|
10380
|
+
const compiled = compileSchema(iterationSchema, dialect);
|
|
10381
|
+
if (compiled.status !== "refused") return;
|
|
10382
|
+
const offending = toolSchemas.find(({ inputSchema }) => compileSchema(inputSchema, dialect).status === "refused");
|
|
10383
|
+
const message = offending ? `[${orgName}] Agent '${agentId}' tool '${offending.tool.name}' ${describeGrammarRefusal(
|
|
10384
|
+
compileSchema(offending.inputSchema, dialect).refusalReasons,
|
|
10385
|
+
offending.inputSchema
|
|
10386
|
+
)}. This silently drops strict-mode enforcement for the agent's ENTIRE iteration schema on every call, not just this tool -- compileSchema refuses whole-schema, never per-tool.` : `[${orgName}] Agent '${agentId}' iteration schema refused strict mode for provider '${agent.modelConfig.provider}': ${compiled.refusalReasons.join(", ")}. No single tool is independently responsible -- this is a whole-schema limit (e.g. total optional properties across every tool exceeding the provider's cap).`;
|
|
10387
|
+
if (getResourceValidatorMode() === "strict") {
|
|
10388
|
+
throw new RegistryValidationError(orgName, agentId, "tools", message);
|
|
10389
|
+
}
|
|
10390
|
+
console.warn(message);
|
|
10391
|
+
}
|
|
10392
|
+
function validateAgentCheapAssertions(orgName, agentId, agent) {
|
|
10393
|
+
const issues = [];
|
|
10394
|
+
const config2 = agent.config;
|
|
10395
|
+
if (config2.sessionCapable && config2.securityLevel === "none") {
|
|
10396
|
+
issues.push(
|
|
10397
|
+
`securityLevel: 'none' on a sessionCapable agent -- a session agent takes untrusted user input and must run with prompt-injection defenses ('standard' or 'hardened').`
|
|
10398
|
+
);
|
|
10399
|
+
}
|
|
10400
|
+
if (!isStubDefinition(agent) && !config2.systemPrompt.trim()) {
|
|
10401
|
+
issues.push(`systemPrompt is empty -- an agent with no behavioural instructions cannot be deployed.`);
|
|
10402
|
+
}
|
|
10403
|
+
const maxIterations = config2.constraints?.maxIterations;
|
|
10404
|
+
if (maxIterations !== void 0 && maxIterations < 1) {
|
|
10405
|
+
issues.push(`constraints.maxIterations is ${maxIterations} -- an agent needs at least 1 iteration to run.`);
|
|
10406
|
+
}
|
|
10407
|
+
const descriptorAgentKind = config2.resource?.agentKind;
|
|
10408
|
+
if (!isStubDefinition(agent) && descriptorAgentKind !== void 0 && descriptorAgentKind !== config2.kind) {
|
|
10409
|
+
issues.push(
|
|
10410
|
+
`config.kind ('${config2.kind}') does not match its OM resource descriptor's agentKind ('${descriptorAgentKind}') -- these are documented as mirrors of each other.`
|
|
10411
|
+
);
|
|
10412
|
+
}
|
|
10413
|
+
for (const tool of agent.tools) {
|
|
10414
|
+
if (tool.maxOutputTokens !== void 0 && (!Number.isFinite(tool.maxOutputTokens) || tool.maxOutputTokens <= 0)) {
|
|
10415
|
+
issues.push(
|
|
10416
|
+
`tool '${tool.name}' declares maxOutputTokens: ${tool.maxOutputTokens}, which must be a positive number.`
|
|
10417
|
+
);
|
|
10418
|
+
}
|
|
10419
|
+
}
|
|
10420
|
+
if (issues.length === 0) return;
|
|
10421
|
+
const message = `[${orgName}] Agent '${agentId}': ${issues.join(" ")}`;
|
|
10422
|
+
if (getResourceValidatorMode() === "strict") {
|
|
10423
|
+
throw new RegistryValidationError(orgName, agentId, "config", message);
|
|
10424
|
+
}
|
|
10425
|
+
console.warn(message);
|
|
10426
|
+
}
|
|
10105
10427
|
function validateExecutionInterface(orgName, resourceId, executionInterface, inputSchema) {
|
|
10106
10428
|
const form = executionInterface.form;
|
|
10107
10429
|
const fieldMappings = form.fieldMappings ?? {};
|
|
@@ -10542,6 +10864,22 @@ function startWorker(org) {
|
|
|
10542
10864
|
name: a3.config.name,
|
|
10543
10865
|
type: a3.config.type,
|
|
10544
10866
|
resource: a3.config.resource,
|
|
10867
|
+
// Wave O / E3: `kind` and `constraints` never reached the platform stub before this --
|
|
10868
|
+
// every remotely-deployed agent registered as `kind: 'utility'` regardless of what its
|
|
10869
|
+
// author declared (the receiving side, apps/api's ManifestResource, already had a `kind`
|
|
10870
|
+
// field; nothing on this side ever populated it), and every tenant agent ran with the
|
|
10871
|
+
// platform's 2-hour timeout ceiling regardless of its own `constraints.timeout`.
|
|
10872
|
+
kind: a3.config.kind,
|
|
10873
|
+
constraints: a3.config.constraints,
|
|
10874
|
+
// `systemPrompt` and `securityLevel` ride along for the same reason, and the live gate is
|
|
10875
|
+
// what proved it: Wave O4 asserts a non-empty `systemPrompt`, but the stub the platform
|
|
10876
|
+
// builds from this manifest had no such field, so the assertion fired against a stub that
|
|
10877
|
+
// structurally could never satisfy it and rejected EVERY remote agent deploy. Carrying
|
|
10878
|
+
// only `kind` and `constraints` while asserting on a third field is the actual defect.
|
|
10879
|
+
// `securityLevel` is here too so O4's `'none'` + `sessionCapable` check tests the agent's
|
|
10880
|
+
// real tier rather than silently passing on an absent one.
|
|
10881
|
+
systemPrompt: a3.config.systemPrompt,
|
|
10882
|
+
securityLevel: a3.config.securityLevel,
|
|
10545
10883
|
status: a3.config.status,
|
|
10546
10884
|
description: a3.config.description,
|
|
10547
10885
|
version: a3.config.version,
|
|
@@ -10570,7 +10908,7 @@ function startWorker(org) {
|
|
|
10570
10908
|
}
|
|
10571
10909
|
if (msg.type === "abort") {
|
|
10572
10910
|
console.log("[SDK-WORKER] Abort requested by parent");
|
|
10573
|
-
localAbortController.abort();
|
|
10911
|
+
localAbortController.abort(msg.reason);
|
|
10574
10912
|
return;
|
|
10575
10913
|
}
|
|
10576
10914
|
if (msg.type === "execute") {
|
|
@@ -10626,10 +10964,11 @@ function startWorker(org) {
|
|
|
10626
10964
|
const logs = [];
|
|
10627
10965
|
const { restore } = captureConsole(executionId, logs);
|
|
10628
10966
|
const startTime = Date.now();
|
|
10967
|
+
let agentInstance;
|
|
10629
10968
|
try {
|
|
10630
10969
|
console.log(`[SDK-WORKER] Running agent '${resourceId}' (${agentDef.tools.length} tools)`);
|
|
10631
10970
|
const adapterFactory = createPostMessageAdapterFactory();
|
|
10632
|
-
|
|
10971
|
+
agentInstance = new Agent(agentDef, adapterFactory, {
|
|
10633
10972
|
initialMemory: sessionMemory
|
|
10634
10973
|
});
|
|
10635
10974
|
const context = buildWorkerExecutionContext({
|
|
@@ -10663,10 +11002,12 @@ function startWorker(org) {
|
|
|
10663
11002
|
const durationMs = Date.now() - startTime;
|
|
10664
11003
|
const serializedError = serializeWorkerError(err);
|
|
10665
11004
|
console.error(`[SDK-WORKER] Agent '${resourceId}' failed (${durationMs}ms): ${serializedError.error}`);
|
|
11005
|
+
const memorySnapshot = agentInstance?.getMemorySnapshot();
|
|
10666
11006
|
parentPort.postMessage({
|
|
10667
11007
|
type: "result",
|
|
10668
11008
|
status: "failed",
|
|
10669
11009
|
...serializedError,
|
|
11010
|
+
...memorySnapshot ? { memorySnapshot } : {},
|
|
10670
11011
|
logs,
|
|
10671
11012
|
metrics: { durationMs }
|
|
10672
11013
|
});
|