@ziggs-ai/agent-sdk 0.10.8 → 0.11.1
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/CHANGELOG.md +11 -11
- package/README.md +1 -1
- package/dist/AgentHost.d.ts +96 -16
- package/dist/AgentHost.js +205 -25
- package/dist/adapters/AnthropicAdapter.js +1 -1
- package/dist/adapters/McpAdapter.d.ts +10 -10
- package/dist/adapters/McpAdapter.js +31 -21
- package/dist/adapters/OpenAIAdapter.js +1 -1
- package/dist/agent/Agent.d.ts +2 -2
- package/dist/agent/ClaudeSdkAgent.d.ts +15 -6
- package/dist/agent/ClaudeSdkAgent.js +149 -48
- package/dist/agent/claudeCognition.d.ts +3 -3
- package/dist/agent/claudeCognition.js +7 -7
- package/dist/agent/claudeSdkTools.d.ts +3 -3
- package/dist/agent/claudeSdkTools.js +34 -15
- package/dist/agent/hostedAgentConnection.d.ts +1 -1
- package/dist/agent/hostedAgentConnection.js +1 -1
- package/dist/agent/toolManagerToSdkMcp.d.ts +1 -1
- package/dist/agent/toolManagerToSdkMcp.js +2 -2
- package/dist/context/applyEffects.js +1 -1
- package/dist/defineAgent.js +33 -4
- package/dist/formatters/HistoryFormatter.js +2 -2
- package/dist/index.d.ts +4 -2
- package/dist/index.js +8 -5
- package/dist/ingress/normalizeIncoming.d.ts +1 -1
- package/dist/ingress/normalizeIncoming.js +7 -7
- package/dist/pricing/fleetDefaults.d.ts +3 -3
- package/dist/pricing/fleetDefaults.js +3 -3
- package/dist/pricing/fleetEvalFree.d.ts +1 -1
- package/dist/pricing/fleetEvalFree.js +1 -1
- package/dist/pricing/fleetFreeTierA.gen.d.ts +1 -1
- package/dist/pricing/fleetFreeTierA.gen.js +1 -1
- package/dist/pricing/fleetTierByAgentId.gen.d.ts +2 -2
- package/dist/pricing/fleetTierByAgentId.gen.js +9 -2
- package/dist/runtime/AgentMachine.d.ts +1 -1
- package/dist/runtime/AgentMachine.js +8 -8
- package/dist/runtime/PromptBuilder.d.ts +2 -2
- package/dist/runtime/PromptBuilder.js +3 -3
- package/dist/runtime/bootstrapCtx.d.ts +1 -1
- package/dist/runtime/bootstrapCtx.js +2 -2
- package/dist/runtime/buildOutcome.d.ts +1 -1
- package/dist/runtime/buildOutcome.js +4 -4
- package/dist/runtime/defaults.d.ts +5 -5
- package/dist/runtime/defaults.js +4 -4
- package/dist/runtime/deliveryWriteKeys.d.ts +1 -1
- package/dist/runtime/deliveryWriteKeys.js +3 -3
- package/dist/runtime/kickoff.d.ts +3 -3
- package/dist/runtime/kickoff.js +3 -3
- package/dist/runtime/runMechanical.d.ts +1 -1
- package/dist/runtime/runMechanical.js +1 -1
- package/dist/runtime/runTurn.d.ts +1 -1
- package/dist/runtime/runTurn.js +10 -10
- package/dist/runtime/taskBookkeeping.d.ts +3 -3
- package/dist/runtime/taskBookkeeping.js +3 -3
- package/dist/runtime/taskIntake.js +2 -2
- package/dist/server/ConnectionPool.js +1 -1
- package/dist/server/EventQueue.d.ts +1 -1
- package/dist/server/EventQueue.js +1 -1
- package/dist/server/SeenMessages.d.ts +2 -2
- package/dist/server/SeenMessages.js +2 -2
- package/dist/server/ZiggsEffectHandler.d.ts +2 -2
- package/dist/server/ZiggsEffectHandler.js +18 -9
- package/dist/server/agreements/AgreementService.d.ts +3 -8
- package/dist/server/agreements/AgreementService.js +4 -7
- package/dist/server/inboxLoop.d.ts +5 -5
- package/dist/server/inboxLoop.js +27 -11
- package/dist/server/polls/PollRunner.d.ts +36 -1
- package/dist/server/polls/PollRunner.js +42 -2
- package/dist/server/questMatch.d.ts +1 -1
- package/dist/server/questMatch.js +1 -1
- package/dist/server/runLauncher.js +3 -3
- package/dist/server/tasks/TaskService.d.ts +5 -5
- package/dist/server/tools/index.d.ts +1 -0
- package/dist/server/tools/index.js +7 -6
- package/dist/server/tools/tier1/protocolRunner.d.ts +1 -1
- package/dist/server/tools/tier1/protocolRunner.js +19 -19
- package/dist/server/tools/tier1/protocolTools.js +4 -4
- package/dist/server/tools/tier2/artifactTools.js +2 -2
- package/dist/server/tools/tier2/chatTools.js +2 -2
- package/dist/server/tools/tier2/connectionTools.js +3 -3
- package/dist/server/tools/tier2/contextTools.js +1 -1
- package/dist/server/tools/tier2/discoveryTools.js +2 -2
- package/dist/server/tools/tier2/grantsTools.js +1 -1
- package/dist/server/tools/tier2/linkTools.js +3 -3
- package/dist/server/tools/tier2/marketplaceTools.js +1 -1
- package/dist/server/tools/tier2/mcpConnectionTools.d.ts +4 -0
- package/dist/server/tools/tier2/mcpConnectionTools.js +136 -0
- package/dist/server/tools/tier2/paymentTools.js +1 -1
- package/dist/shared/operatorKeyClaims.d.ts +2 -2
- package/dist/shared/operatorKeyClaims.js +1 -1
- package/dist/tasks/protocolRegistry.d.ts +1 -1
- package/dist/tasks/protocolRegistry.js +1 -1
- package/dist/tools/ToolManager.d.ts +1 -1
- package/dist/tools/ToolManager.js +3 -2
- package/dist/tools/capabilityAdapter.d.ts +1 -1
- package/dist/tools/capabilityAdapter.js +3 -3
- package/dist/tools/defineTool.d.ts +7 -0
- package/dist/tools/defineTool.js +13 -1
- package/dist/tools/needs.d.ts +54 -0
- package/dist/tools/needs.js +84 -0
- package/dist/tools/recordReport.d.ts +3 -3
- package/dist/tools/recordReport.js +3 -3
- package/dist/types.d.ts +28 -18
- package/package.json +3 -3
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
|
|
2
2
|
import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js';
|
|
3
3
|
import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js';
|
|
4
|
-
import { ConnectionsClient } from '@ziggs-ai/api-client';
|
|
4
|
+
import { ConnectionsClient, getBackendUrl } from '@ziggs-ai/api-client';
|
|
5
5
|
import { AgentHost } from '../AgentHost.js';
|
|
6
6
|
import { thinkingDefaults } from '../runtime/defaults.js';
|
|
7
7
|
/**
|
|
8
|
-
* Typed failure from the Ziggs MCP gateway
|
|
8
|
+
* Typed failure from the Ziggs MCP gateway. `code` is the stable
|
|
9
9
|
* machine-readable reason — e.g. `GRANT_REVOKED`, `GRANT_EXPIRED`,
|
|
10
10
|
* `GRANT_HOLDER_MISMATCH`, `GRANT_CONNECTION_MISMATCH` — so agent code can
|
|
11
11
|
* branch ("revoked → tell the user and stop retrying" vs "network blip →
|
|
@@ -56,10 +56,15 @@ export function resolveZiggsGatewayTarget(config, defaults = {}) {
|
|
|
56
56
|
if (!agentId) {
|
|
57
57
|
throw new Error('ziggs MCP transport requires an agentId to impersonate (X-Agent-Id)');
|
|
58
58
|
}
|
|
59
|
+
// Explicit overrides still win, but the LAST resort is `getBackendUrl()` —
|
|
60
|
+
// the resolution every other SDK call uses — not `localhost:3000`. A deployment
|
|
61
|
+
// that sets none of BACKEND_URL / ZIGGS_BACKEND_URL would otherwise have every
|
|
62
|
+
// gateway call silently point at localhost inside its own container: the
|
|
63
|
+
// requests never reach the backend, and the agent reports it as unreachable.
|
|
59
64
|
const baseUrl = (config.baseUrl ||
|
|
60
|
-
process.env.BACKEND_URL ||
|
|
61
65
|
process.env.ZIGGS_BACKEND_URL ||
|
|
62
|
-
|
|
66
|
+
process.env.BACKEND_URL ||
|
|
67
|
+
getBackendUrl()).replace(/\/$/, '');
|
|
63
68
|
return {
|
|
64
69
|
url: new URL(`${baseUrl}/connections/${encodeURIComponent(config.connectionId)}/mcp`),
|
|
65
70
|
headers: {
|
|
@@ -70,7 +75,7 @@ export function resolveZiggsGatewayTarget(config, defaults = {}) {
|
|
|
70
75
|
};
|
|
71
76
|
}
|
|
72
77
|
/**
|
|
73
|
-
* Resolve the per-user gateway URL + headers
|
|
78
|
+
* Resolve the per-user gateway URL + headers. No connectionId/grantId —
|
|
74
79
|
* the gateway resolves the invoking user's own connection + grant from
|
|
75
80
|
* `x-on-behalf-of-user` + the impersonated agent.
|
|
76
81
|
*/
|
|
@@ -79,17 +84,22 @@ export function resolvePerUserGatewayTarget(config, agentId, userId, onBehalfOfA
|
|
|
79
84
|
if (!operatorKey) {
|
|
80
85
|
throw new Error('ziggs-per-user MCP transport requires an operatorKey (or ZIGGS_OPERATOR_KEY)');
|
|
81
86
|
}
|
|
87
|
+
// Explicit overrides still win, but the LAST resort is `getBackendUrl()` —
|
|
88
|
+
// the resolution every other SDK call uses — not `localhost:3000`. A deployment
|
|
89
|
+
// that sets none of BACKEND_URL / ZIGGS_BACKEND_URL would otherwise have every
|
|
90
|
+
// gateway call silently point at localhost inside its own container: the
|
|
91
|
+
// requests never reach the backend, and the agent reports it as unreachable.
|
|
82
92
|
const baseUrl = (config.baseUrl ||
|
|
83
|
-
process.env.BACKEND_URL ||
|
|
84
93
|
process.env.ZIGGS_BACKEND_URL ||
|
|
85
|
-
|
|
94
|
+
process.env.BACKEND_URL ||
|
|
95
|
+
getBackendUrl()).replace(/\/$/, '');
|
|
86
96
|
return {
|
|
87
97
|
url: new URL(`${baseUrl}/connections/mcp/relay`),
|
|
88
98
|
headers: {
|
|
89
99
|
authorization: `Bearer ${operatorKey}`,
|
|
90
100
|
'x-agent-id': agentId,
|
|
91
101
|
'x-on-behalf-of-user': userId,
|
|
92
|
-
//
|
|
102
|
+
// when this call runs under a delegation, userId is the delegating
|
|
93
103
|
// agent (senderId), not the end-user who owns the connection. The gateway
|
|
94
104
|
// uses this agreement to resolve the delegation root's user.
|
|
95
105
|
...(onBehalfOfAgreementId ? { 'x-on-behalf-of-agreement': onBehalfOfAgreementId } : {}),
|
|
@@ -103,7 +113,7 @@ const CONTROL_TOOL_RESERVE = 4;
|
|
|
103
113
|
/**
|
|
104
114
|
* How many MCP tools may be bound to the LLM, given the provider cap and the
|
|
105
115
|
* number of extra (non-MCP) tools the agent also carries. Always >= 1 so a
|
|
106
|
-
* pathological config still binds something rather than nothing.
|
|
116
|
+
* pathological config still binds something rather than nothing.
|
|
107
117
|
*/
|
|
108
118
|
export function mcpToolBudget(maxTools, extraToolCount) {
|
|
109
119
|
return Math.max(1, maxTools - CONTROL_TOOL_RESERVE - extraToolCount);
|
|
@@ -218,7 +228,7 @@ function buildDefaultWorkflow(description, mcpTools) {
|
|
|
218
228
|
* that full block rides every turn. Keep the lead — what the tool does + how to
|
|
219
229
|
* call it — and drop the embedded reference; agents fetch detail via dedicated
|
|
220
230
|
* schema/help tools at call time. Tunable; big enough to preserve real guidance.
|
|
221
|
-
|
|
231
|
+
*.
|
|
222
232
|
*/
|
|
223
233
|
export const MAX_TOOL_DESC_CHARS = 2000;
|
|
224
234
|
/** Truncate one description at a sentence/newline boundary near the cap. */
|
|
@@ -230,7 +240,7 @@ export function trimDescription(desc) {
|
|
|
230
240
|
const head = boundary > MAX_TOOL_DESC_CHARS * 0.6 ? slice.slice(0, boundary + 1) : slice;
|
|
231
241
|
return `${head.trimEnd()} […description truncated; use the server's schema/help tools for full detail]`;
|
|
232
242
|
}
|
|
233
|
-
/** Trim oversized descriptions across the tool set (cost control
|
|
243
|
+
/** Trim oversized descriptions across the tool set (cost control). */
|
|
234
244
|
function trimToolDescriptions(tools, agentId) {
|
|
235
245
|
let trimmedCount = 0;
|
|
236
246
|
let savedChars = 0;
|
|
@@ -243,14 +253,14 @@ function trimToolDescriptions(tools, agentId) {
|
|
|
243
253
|
return { ...t, description: nextDesc };
|
|
244
254
|
});
|
|
245
255
|
if (trimmedCount > 0) {
|
|
246
|
-
console.warn(`[McpAdapter] ${agentId}: trimmed ${trimmedCount} oversized tool description(s) to ${MAX_TOOL_DESC_CHARS} chars, saving ~${Math.round(savedChars / 4 / 1000)}K tokens/turn
|
|
256
|
+
console.warn(`[McpAdapter] ${agentId}: trimmed ${trimmedCount} oversized tool description(s) to ${MAX_TOOL_DESC_CHARS} chars, saving ~${Math.round(savedChars / 4 / 1000)}K tokens/turn.`);
|
|
247
257
|
}
|
|
248
258
|
return out;
|
|
249
259
|
}
|
|
250
260
|
/**
|
|
251
261
|
* Keep only tools whose name matches one of the allowlist substrings
|
|
252
262
|
* (case-insensitive). No allowlist → pass through unchanged. Applied before the
|
|
253
|
-
* maxTools cap so a targeted agent binds exactly the tools it needs.
|
|
263
|
+
* maxTools cap so a targeted agent binds exactly the tools it needs.
|
|
254
264
|
*/
|
|
255
265
|
function filterToolsByAllowlist(tools, allowedTools, agentId) {
|
|
256
266
|
if (!allowedTools || allowedTools.length === 0)
|
|
@@ -263,7 +273,7 @@ function filterToolsByAllowlist(tools, allowedTools, agentId) {
|
|
|
263
273
|
/**
|
|
264
274
|
* Cap the tool set bound to the LLM at the provider limit (OpenAI 128) —
|
|
265
275
|
* truncate and log rather than crash — and trim oversized descriptions so the
|
|
266
|
-
* bound schema doesn't ship a manual every turn.
|
|
276
|
+
* bound schema doesn't ship a manual every turn. Phase 1. Shared by the
|
|
267
277
|
* boot-time (single-client) and per-user paths. When `allowedTools` is set the
|
|
268
278
|
* server's tools are narrowed by name first, so the cap rarely bites.
|
|
269
279
|
*/
|
|
@@ -273,7 +283,7 @@ function capTools(tools, agentId, maxTools, extraToolCount, allowedTools) {
|
|
|
273
283
|
const budget = mcpToolBudget(maxTools, extraToolCount);
|
|
274
284
|
if (trimmed.length <= budget)
|
|
275
285
|
return trimmed;
|
|
276
|
-
console.warn(`[McpAdapter] ${agentId}: MCP server exposes ${trimmed.length} tools, over the ${maxTools}-tool LLM cap; binding the first ${budget}, dropping ${trimmed.length - budget}
|
|
286
|
+
console.warn(`[McpAdapter] ${agentId}: MCP server exposes ${trimmed.length} tools, over the ${maxTools}-tool LLM cap; binding the first ${budget}, dropping ${trimmed.length - budget}.`);
|
|
277
287
|
return trimmed.slice(0, budget);
|
|
278
288
|
}
|
|
279
289
|
/**
|
|
@@ -311,7 +321,7 @@ function capTools(tools, agentId, maxTools, extraToolCount, allowedTools) {
|
|
|
311
321
|
*/
|
|
312
322
|
export async function createMcpAgent(options) {
|
|
313
323
|
const { transport: transportConfig, extraTools = [], workflow, maxTools = DEFAULT_MAX_TOOLS, allowedTools, toolSchema, ...hostOptions } = options;
|
|
314
|
-
// ── Per-user path
|
|
324
|
+
// ── Per-user path: no boot connection; one MCP client per invoking
|
|
315
325
|
// user, each carrying that user's on-behalf-of header for an isolated session.
|
|
316
326
|
if (transportConfig.type === 'ziggs-per-user') {
|
|
317
327
|
const agentId = transportConfig.agentId || hostOptions.agentId;
|
|
@@ -351,12 +361,12 @@ export async function createMcpAgent(options) {
|
|
|
351
361
|
// When the invoking user hasn't connected this server, open a connection
|
|
352
362
|
// request targeting THEM (X-On-Behalf-Of-User) so the consent prompt lands
|
|
353
363
|
// in their inbox. The backend dedups pending requests, so retrying every
|
|
354
|
-
// turn is safe.
|
|
364
|
+
// turn is safe.
|
|
355
365
|
const backendBase = (transportConfig.baseUrl ||
|
|
356
366
|
process.env.BACKEND_URL ||
|
|
357
367
|
process.env.ZIGGS_BACKEND_URL ||
|
|
358
368
|
'http://localhost:3000').replace(/\/$/, '');
|
|
359
|
-
// App URL for the user-facing connect link
|
|
369
|
+
// App URL for the user-facing connect link: api.ziggsai.com → ziggsai.com.
|
|
360
370
|
const appUrl = backendBase.includes('localhost')
|
|
361
371
|
? 'http://localhost:1234'
|
|
362
372
|
: backendBase.replace(/\/\/api\./, '//');
|
|
@@ -364,7 +374,7 @@ export async function createMcpAgent(options) {
|
|
|
364
374
|
? new URL(transportConfig.serverUrl).host
|
|
365
375
|
: 'the external service';
|
|
366
376
|
// Open a connection request for this user; returns its id so the caller can
|
|
367
|
-
// deep-link the user straight to it on the Connections page
|
|
377
|
+
// deep-link the user straight to it on the Connections page.
|
|
368
378
|
const triggerConnectionRequest = async (userId) => {
|
|
369
379
|
if (!transportConfig.serverUrl)
|
|
370
380
|
return null;
|
|
@@ -390,11 +400,11 @@ export async function createMcpAgent(options) {
|
|
|
390
400
|
if (!userId) {
|
|
391
401
|
throw new Error('per-user MCP tool call requires an invoking user (context.senderId is missing)');
|
|
392
402
|
}
|
|
393
|
-
//
|
|
403
|
+
// for a delegated wake, senderId is the delegating agent, not the
|
|
394
404
|
// connection-owning end-user; pass the agreement so the gateway resolves
|
|
395
405
|
// the delegation root's user.
|
|
396
406
|
const onBehalfOfAgreementId = typeof context.onBehalfOfAgreementId === 'string' ? context.onBehalfOfAgreementId : undefined;
|
|
397
|
-
//
|
|
407
|
+
// (B): per-user resolution is intermittently flaky even when the
|
|
398
408
|
// connection is valid and resolvable — the same on-behalf call fails with
|
|
399
409
|
// NO_USER_CONNECTION on one wake and succeeds on the next. clientFor never
|
|
400
410
|
// caches a failed connect, so each attempt re-resolves from scratch. Retry
|
|
@@ -137,7 +137,7 @@ export class OpenAIAdapter {
|
|
|
137
137
|
stream: true,
|
|
138
138
|
// Without this, a streamed completion never returns a usage block — the
|
|
139
139
|
// streamed reply (often the largest output) would bill tokens that are
|
|
140
|
-
// counted nowhere
|
|
140
|
+
// counted nowhere. The usage arrives on a final chunk whose
|
|
141
141
|
// `choices` is empty.
|
|
142
142
|
stream_options: { include_usage: true },
|
|
143
143
|
max_completion_tokens: maxCompletionTokensForModel(this.model),
|
package/dist/agent/Agent.d.ts
CHANGED
|
@@ -25,7 +25,7 @@ export interface TickInput {
|
|
|
25
25
|
* choice — `counterparty:<id>`, `agreement:<id>:notes`, etc.
|
|
26
26
|
*/
|
|
27
27
|
memory?: MemoryStore;
|
|
28
|
-
/** Optional tap for FSM transition events
|
|
28
|
+
/** Optional tap for FSM transition events. */
|
|
29
29
|
transitionTap?: (event: TransitionEvent) => void;
|
|
30
30
|
/**
|
|
31
31
|
* Aborted by the host when the tick watchdog fires. A brain driving an
|
|
@@ -49,7 +49,7 @@ export interface TickOutput {
|
|
|
49
49
|
/**
|
|
50
50
|
* Accumulated LLM call count and token usage for this tick.
|
|
51
51
|
* `cacheRead` / `cacheCreation` are populated by the Claude brain (its usage
|
|
52
|
-
* exposes cache traffic, which is the bulk of real cost
|
|
52
|
+
* exposes cache traffic, which is the bulk of real cost/813); the
|
|
53
53
|
* FSM path leaves them undefined. `total` includes cache tokens when present.
|
|
54
54
|
*/
|
|
55
55
|
metrics: {
|
|
@@ -4,7 +4,7 @@ import type { ToolManager } from '../tools/ToolManager.js';
|
|
|
4
4
|
import { type RawEvent } from '../runtime/buildOutcome.js';
|
|
5
5
|
import type { TickInput, TickOutput } from './Agent.js';
|
|
6
6
|
/**
|
|
7
|
-
*
|
|
7
|
+
* status notices must not spend a Claude turn answering
|
|
8
8
|
* nobody. Real work wakes arrive as chat `message`, synthetic `task_result`
|
|
9
9
|
* from `wakeTaskAssignment`, or `agreement_lifecycle` (hire / proposal
|
|
10
10
|
* resolution — the FSM kickoffs on the same event; Claude must act too).
|
|
@@ -28,7 +28,7 @@ export interface ClaudeSdkAgentOptions {
|
|
|
28
28
|
toolManager: ToolManager;
|
|
29
29
|
/** Store-facing one-liner. Used as the role block only when `systemPrompt` is absent. */
|
|
30
30
|
description: string;
|
|
31
|
-
/** The agent's system prompt
|
|
31
|
+
/** The agent's system prompt. Preferred over `description` for the role block. */
|
|
32
32
|
systemPrompt?: string;
|
|
33
33
|
specialization?: string | null;
|
|
34
34
|
model: string;
|
|
@@ -36,7 +36,7 @@ export interface ClaudeSdkAgentOptions {
|
|
|
36
36
|
operatorKey: string;
|
|
37
37
|
agentId: string;
|
|
38
38
|
maxTurns?: number;
|
|
39
|
-
/**
|
|
39
|
+
/** when true, never resume the prior Claude session — each wake is a
|
|
40
40
|
* fresh transcript. Keeps per-wake input flat on step-driven orchestrators
|
|
41
41
|
* whose wakes already carry their state. */
|
|
42
42
|
statelessPerWake?: boolean;
|
|
@@ -44,7 +44,7 @@ export interface ClaudeSdkAgentOptions {
|
|
|
44
44
|
queryFn?: typeof query;
|
|
45
45
|
}
|
|
46
46
|
/**
|
|
47
|
-
*
|
|
47
|
+
* "the transcript you asked me to resume is not here".
|
|
48
48
|
*
|
|
49
49
|
* Claude Code answers a `resume` of an unknown id with this, and it is the only
|
|
50
50
|
* failure that a fresh session reliably fixes: the id is valid-looking but its
|
|
@@ -52,12 +52,21 @@ export interface ClaudeSdkAgentOptions {
|
|
|
52
52
|
* that is all the CLI gives us — no code, no typed error — so keep it narrow
|
|
53
53
|
* enough that a different failure never claims the retry.
|
|
54
54
|
*/
|
|
55
|
+
/**
|
|
56
|
+
* "I ran out of turns", as Claude Code reports it.
|
|
57
|
+
*
|
|
58
|
+
* Turn exhaustion is not a crash: the wake did real work and stopped at a cap
|
|
59
|
+
* we set. It arrives as a thrown error result with no code and no type, so the
|
|
60
|
+
* message is all there is to match on — kept narrow so no other failure claims
|
|
61
|
+
* the handling.
|
|
62
|
+
*/
|
|
63
|
+
export declare function isMaxTurnsError(err: unknown): boolean;
|
|
55
64
|
export declare function isMissingSessionError(err: unknown): boolean;
|
|
56
65
|
/**
|
|
57
66
|
* A Ziggs agent whose brain is the Claude Agent SDK. Same host, same `eff`
|
|
58
67
|
* side channel, same tool surface as the FSM `Agent` — the ONLY difference is
|
|
59
68
|
* the loop: instead of the FSM + `runTurn`, a wake drives a headless Claude
|
|
60
|
-
* `query()` (see
|
|
69
|
+
* `query()` (see "host is the product; the brain is how it decides").
|
|
61
70
|
*/
|
|
62
71
|
export declare class ClaudeSdkAgent {
|
|
63
72
|
workflow: Workflow;
|
|
@@ -72,7 +81,7 @@ export declare class ClaudeSdkAgent {
|
|
|
72
81
|
private statelessPerWake;
|
|
73
82
|
private queryFn;
|
|
74
83
|
/**
|
|
75
|
-
*
|
|
84
|
+
* #7 — tool set is fixed at host construction; rebuilding the
|
|
76
85
|
* in-process MCP server (+ zod schema tree) every wake was pure waste.
|
|
77
86
|
* A mutable holder supplies per-wake context without closing over `tick`.
|
|
78
87
|
*/
|
|
@@ -9,7 +9,7 @@ import { claudeQueryOptions, buildClaudeEventPrompt, summarizeContext, extractAs
|
|
|
9
9
|
import { assertHostedAgentZiggsTools } from './hostedAgentConnection.js';
|
|
10
10
|
import { runtimeLog } from '../shared/runtimeLog.js';
|
|
11
11
|
/**
|
|
12
|
-
*
|
|
12
|
+
* status notices must not spend a Claude turn answering
|
|
13
13
|
* nobody. Real work wakes arrive as chat `message`, synthetic `task_result`
|
|
14
14
|
* from `wakeTaskAssignment`, or `agreement_lifecycle` (hire / proposal
|
|
15
15
|
* resolution — the FSM kickoffs on the same event; Claude must act too).
|
|
@@ -35,30 +35,34 @@ export function isNoticeOnlyWake(event, ownAgentId) {
|
|
|
35
35
|
}
|
|
36
36
|
const DEFAULT_STATE = 'ready';
|
|
37
37
|
/**
|
|
38
|
-
* Per-
|
|
39
|
-
*
|
|
40
|
-
*
|
|
41
|
-
*
|
|
42
|
-
*
|
|
43
|
-
*
|
|
44
|
-
|
|
38
|
+
* Per-wake turn cap — a runaway backstop, NOT a budget.
|
|
39
|
+
*
|
|
40
|
+
* Cost is bounded by the priced spend guard below, so this only has to sit above
|
|
41
|
+
* the longest honest job, with room for a retry inside it. A job that gathers
|
|
42
|
+
* before it works — reading the engagement, listing grants, discovering a
|
|
43
|
+
* connection's tools, pulling the data — spends around ten turns before it
|
|
44
|
+
* starts on the work it was hired for, then needs several more to draft, record
|
|
45
|
+
* the artifact and close the task. Set below that, the cap ends wakes with the
|
|
46
|
+
* gathering done and nothing filed.
|
|
47
|
+
*/
|
|
48
|
+
const DEFAULT_MAX_TURNS = Number(process.env.CLAUDE_SDK_MAX_TURNS ?? 30);
|
|
45
49
|
/**
|
|
46
50
|
* Hard ceiling on billable input tokens (incl. cache read/write) a single wake
|
|
47
|
-
* may spend before we abort the query. Defense-in-depth on the
|
|
51
|
+
* may spend before we abort the query. Defense-in-depth on the host's LLM key
|
|
48
52
|
* against a runaway loop; set high enough that normal tasks never trip it. The
|
|
49
|
-
* task ends unfinished (and logs loudly) rather than burning unbounded.
|
|
53
|
+
* task ends unfinished (and logs loudly) rather than burning unbounded. */
|
|
50
54
|
const MAX_TASK_INPUT_TOKENS = Number(process.env.CLAUDE_SDK_MAX_INPUT_TOKENS ?? 500_000);
|
|
51
55
|
/**
|
|
52
56
|
* Per-lane wake-rate circuit breaker. The per-wake caps above bound one wake;
|
|
53
|
-
* nothing
|
|
54
|
-
*
|
|
55
|
-
*
|
|
56
|
-
*
|
|
57
|
-
*
|
|
57
|
+
* nothing bounds the NUMBER of wakes. An echo path — anything that turns an
|
|
58
|
+
* agent's own write back into a delivery addressed to it — drives one wake per
|
|
59
|
+
* wake, forever. Targeting never routes an agent its own writes, so this is a
|
|
60
|
+
* backstop: a lane waking faster than real traffic plausibly drives it skips the
|
|
61
|
+
* LLM (and logs loudly) instead of draining the key. */
|
|
58
62
|
const WAKE_WINDOW_MS = 10 * 60_000;
|
|
59
63
|
const MAX_WAKES_PER_WINDOW = Number(process.env.CLAUDE_SDK_MAX_WAKES_PER_WINDOW ?? 40);
|
|
60
64
|
/**
|
|
61
|
-
* Resumed-transcript ceiling
|
|
65
|
+
* Resumed-transcript ceiling. Each wake resumes the lane's Claude
|
|
62
66
|
* session, so the whole transcript (event prompts, tool results, summaries)
|
|
63
67
|
* replays on every turn — mostly as cache reads, but it grows without bound on
|
|
64
68
|
* a long-lived lane and dominates per-wake cost. When a wake's billable input
|
|
@@ -69,7 +73,7 @@ const MAX_WAKES_PER_WINDOW = Number(process.env.CLAUDE_SDK_MAX_WAKES_PER_WINDOW
|
|
|
69
73
|
* before it can approach the per-wake abort. */
|
|
70
74
|
const MAX_SESSION_RESUME_INPUT_TOKENS = Number(process.env.CLAUDE_SDK_MAX_SESSION_INPUT_TOKENS ?? 200_000);
|
|
71
75
|
/**
|
|
72
|
-
*
|
|
76
|
+
* "the transcript you asked me to resume is not here".
|
|
73
77
|
*
|
|
74
78
|
* Claude Code answers a `resume` of an unknown id with this, and it is the only
|
|
75
79
|
* failure that a fresh session reliably fixes: the id is valid-looking but its
|
|
@@ -77,6 +81,19 @@ const MAX_SESSION_RESUME_INPUT_TOKENS = Number(process.env.CLAUDE_SDK_MAX_SESSIO
|
|
|
77
81
|
* that is all the CLI gives us — no code, no typed error — so keep it narrow
|
|
78
82
|
* enough that a different failure never claims the retry.
|
|
79
83
|
*/
|
|
84
|
+
/**
|
|
85
|
+
* "I ran out of turns", as Claude Code reports it.
|
|
86
|
+
*
|
|
87
|
+
* Turn exhaustion is not a crash: the wake did real work and stopped at a cap
|
|
88
|
+
* we set. It arrives as a thrown error result with no code and no type, so the
|
|
89
|
+
* message is all there is to match on — kept narrow so no other failure claims
|
|
90
|
+
* the handling.
|
|
91
|
+
*/
|
|
92
|
+
export function isMaxTurnsError(err) {
|
|
93
|
+
const message = err?.message;
|
|
94
|
+
return (typeof message === 'string' &&
|
|
95
|
+
/reached maximum number of turns/i.test(message));
|
|
96
|
+
}
|
|
80
97
|
export function isMissingSessionError(err) {
|
|
81
98
|
const message = err?.message;
|
|
82
99
|
return (typeof message === 'string' &&
|
|
@@ -90,6 +107,32 @@ function billableInput(usage) {
|
|
|
90
107
|
(usage.cache_read_input_tokens ?? 0) +
|
|
91
108
|
(usage.cache_creation_input_tokens ?? 0));
|
|
92
109
|
}
|
|
110
|
+
/**
|
|
111
|
+
* The same tokens, priced.
|
|
112
|
+
*
|
|
113
|
+
* A cache READ bills at ~0.1x base input and a cache WRITE at ~1.25x. The spend
|
|
114
|
+
* guard below exists to stop a runaway from draining the key, so it has to count
|
|
115
|
+
* what the key is actually charged — and counting a cache read as a full input
|
|
116
|
+
* token makes every multi-turn wake look like a runaway. The transcript replays
|
|
117
|
+
* on every turn, so a 40k context over ten turns reads as 400k of "spend" while
|
|
118
|
+
* costing about a tenth of that: unpriced, the guard fires on ordinary work long
|
|
119
|
+
* before any runaway would, and it fires mid-job — right after the gathering
|
|
120
|
+
* calls and before the deliverable, which is the whole of "the wake ends before
|
|
121
|
+
* it files anything".
|
|
122
|
+
*
|
|
123
|
+
* Weights, not prices: the ratio between the three input kinds is stable across
|
|
124
|
+
* Claude models even as the per-million rates change, so this stays right
|
|
125
|
+
* without tracking a price table.
|
|
126
|
+
*/
|
|
127
|
+
const CACHE_READ_WEIGHT = 0.1;
|
|
128
|
+
const CACHE_WRITE_WEIGHT = 1.25;
|
|
129
|
+
function costWeightedInput(usage) {
|
|
130
|
+
if (!usage)
|
|
131
|
+
return 0;
|
|
132
|
+
return ((usage.input_tokens ?? 0) +
|
|
133
|
+
(usage.cache_read_input_tokens ?? 0) * CACHE_READ_WEIGHT +
|
|
134
|
+
(usage.cache_creation_input_tokens ?? 0) * CACHE_WRITE_WEIGHT);
|
|
135
|
+
}
|
|
93
136
|
/**
|
|
94
137
|
* Per-API-call usage. Only assistant turns carry it — the result message's
|
|
95
138
|
* usage is the CUMULATIVE total of the whole query, so adding it on top of
|
|
@@ -106,12 +149,12 @@ function extractMetrics(message) {
|
|
|
106
149
|
return null;
|
|
107
150
|
const usage = message.usage;
|
|
108
151
|
// Billable input includes cache reads/writes — that is what the Anthropic
|
|
109
|
-
// console counts and what the
|
|
110
|
-
//
|
|
152
|
+
// console counts and what the key is charged for. Reporting only the uncached
|
|
153
|
+
// slice understates real usage by more than an order of magnitude.
|
|
111
154
|
const input = billableInput(usage);
|
|
112
155
|
const output = usage?.output_tokens ?? 0;
|
|
113
156
|
// Cache read/write are already folded into `input` above; surfaced separately
|
|
114
|
-
// here for per-tick visibility (metricsTap metadata / dashboards
|
|
157
|
+
// here for per-tick visibility (metricsTap metadata / dashboards).
|
|
115
158
|
const cacheRead = usage?.cache_read_input_tokens ?? 0;
|
|
116
159
|
const cacheCreation = usage?.cache_creation_input_tokens ?? 0;
|
|
117
160
|
return {
|
|
@@ -121,8 +164,8 @@ function extractMetrics(message) {
|
|
|
121
164
|
};
|
|
122
165
|
}
|
|
123
166
|
/**
|
|
124
|
-
* Resolve the agreement this wake acts under, for a delegated per-user MCP
|
|
125
|
-
*
|
|
167
|
+
* Resolve the agreement this wake acts under, for a delegated per-user MCP
|
|
168
|
+
* call. Without it the gateway resolves the connection owner from
|
|
126
169
|
* `senderId` — which, under a delegation, is the delegating agent (who owns no
|
|
127
170
|
* connection) → a spurious NO_USER_CONNECTION even though the payer IS connected.
|
|
128
171
|
*
|
|
@@ -130,7 +173,7 @@ function extractMetrics(message) {
|
|
|
130
173
|
* `task_result` events. In practice the worker often fetches while reacting to
|
|
131
174
|
* the proposal-approved / agreement-active lifecycle events, which carry the id
|
|
132
175
|
* at the event top level rather than under `result`. Reading only
|
|
133
|
-
* `result.agreementId` (the original
|
|
176
|
+
* `result.agreementId` (the original wiring) left those wakes with no
|
|
134
177
|
* agreement → the false "authorize your connection" prompt. Resolve from every
|
|
135
178
|
* reliable source instead:
|
|
136
179
|
* 1. task_result events → `result.agreementId`
|
|
@@ -170,7 +213,7 @@ function resolveOnBehalfAgreementId(rawEvent, contextSnap, agentId, sessionId) {
|
|
|
170
213
|
* A Ziggs agent whose brain is the Claude Agent SDK. Same host, same `eff`
|
|
171
214
|
* side channel, same tool surface as the FSM `Agent` — the ONLY difference is
|
|
172
215
|
* the loop: instead of the FSM + `runTurn`, a wake drives a headless Claude
|
|
173
|
-
* `query()` (see
|
|
216
|
+
* `query()` (see "host is the product; the brain is how it decides").
|
|
174
217
|
*/
|
|
175
218
|
export class ClaudeSdkAgent {
|
|
176
219
|
workflow;
|
|
@@ -185,7 +228,7 @@ export class ClaudeSdkAgent {
|
|
|
185
228
|
statelessPerWake;
|
|
186
229
|
queryFn;
|
|
187
230
|
/**
|
|
188
|
-
*
|
|
231
|
+
* #7 — tool set is fixed at host construction; rebuilding the
|
|
189
232
|
* in-process MCP server (+ zod schema tree) every wake was pure waste.
|
|
190
233
|
* A mutable holder supplies per-wake context without closing over `tick`.
|
|
191
234
|
*/
|
|
@@ -200,7 +243,7 @@ export class ClaudeSdkAgent {
|
|
|
200
243
|
throw new Error('ClaudeSdkAgent: anthropicKey is required');
|
|
201
244
|
this.workflow = opts.workflow;
|
|
202
245
|
this.toolManager = opts.toolManager;
|
|
203
|
-
//
|
|
246
|
+
// the prompt and the store blurb are two different strings. Agents
|
|
204
247
|
// written before the split still carry the prompt in `description`, so fall
|
|
205
248
|
// back to it rather than silently dropping their role.
|
|
206
249
|
this.rolePrompt = opts.systemPrompt || opts.description;
|
|
@@ -249,7 +292,7 @@ export class ClaudeSdkAgent {
|
|
|
249
292
|
}
|
|
250
293
|
let contextSnap = null;
|
|
251
294
|
try {
|
|
252
|
-
//
|
|
295
|
+
// #5: pre-read is for wake gating + summarizeContext (a count) +
|
|
253
296
|
// onBehalf agreement resolution — same 12-message cap as the tool path.
|
|
254
297
|
// Uncapped pulls burned 35–47k input tokens/wake on long chats.
|
|
255
298
|
contextSnap = await eff({
|
|
@@ -265,7 +308,7 @@ export class ClaudeSdkAgent {
|
|
|
265
308
|
// no longer resolves (deleted / not visible) has no world to act in —
|
|
266
309
|
// skip the query instead of burning a wake on a fabricated empty history.
|
|
267
310
|
//
|
|
268
|
-
//
|
|
311
|
+
// a bare agreement id (`agr_…`, not the `agrn-…` notification
|
|
269
312
|
// chat) is not a chat. If a wake still keys the lane that way, read-context
|
|
270
313
|
// returns chatMissing and used to continue on an empty snapshot with no
|
|
271
314
|
// signal. Loud-log that mis-key; message wakes still skip below.
|
|
@@ -297,7 +340,7 @@ export class ClaudeSdkAgent {
|
|
|
297
340
|
this.specialization ? `Specialization: ${this.specialization}` : '',
|
|
298
341
|
].filter(Boolean).join('\n');
|
|
299
342
|
// Two blocks, not one string: the shared protocol first so its cache prefix
|
|
300
|
-
// is reachable by every hosted agent, this agent's role second
|
|
343
|
+
// is reachable by every hosted agent, this agent's role second.
|
|
301
344
|
const systemPrompt = [CLAUDE_SDK_PROTOCOL, roleBlock];
|
|
302
345
|
const runId = crypto.randomUUID();
|
|
303
346
|
// Replay-stable write keys for this wake (runId is random and can't serve):
|
|
@@ -312,7 +355,7 @@ export class ClaudeSdkAgent {
|
|
|
312
355
|
eff,
|
|
313
356
|
senderId: rawEvent?.senderId != null ? String(rawEvent.senderId) : undefined,
|
|
314
357
|
senderType: rawEvent?.senderType != null ? String(rawEvent.senderType) : undefined,
|
|
315
|
-
|
|
358
|
+
///832: agreement this wake acts under → lets a delegated per-user
|
|
316
359
|
// MCP call resolve the originating end-user (payer) rather than the
|
|
317
360
|
// delegating agent (senderId). Resolved from every reliable wire source,
|
|
318
361
|
// not just task_result events — see resolveOnBehalfAgreementId.
|
|
@@ -351,8 +394,17 @@ export class ClaudeSdkAgent {
|
|
|
351
394
|
});
|
|
352
395
|
let metrics = { llmCalls: 0, tokens: { total: 0, input: 0, output: 0, cacheRead: 0, cacheCreation: 0 } };
|
|
353
396
|
let sessionId = nextCtx.claudeSessionId;
|
|
397
|
+
// Cost-weighted, cumulative: what this wake has charged the key so far.
|
|
354
398
|
let spentInput = 0;
|
|
355
|
-
//
|
|
399
|
+
// The transcript's SIZE, which is a different question from spend and needs
|
|
400
|
+
// a different measure: the largest single turn's input, unweighted. On the
|
|
401
|
+
// cumulative sum a lane with a modest transcript crosses the resume ceiling
|
|
402
|
+
// simply by taking enough turns, and forgets its session after every wake —
|
|
403
|
+
// so each wake re-gathers from zero and stalls at the same step.
|
|
404
|
+
let peakTurnInput = 0;
|
|
405
|
+
// Why the wake stopped, when it stopped on a guard rather than on its own.
|
|
406
|
+
let abortedBy = null;
|
|
407
|
+
// stateless-per-wake never resumes — each wake is a fresh
|
|
356
408
|
// transcript, so per-wake input stays flat instead of replaying the whole
|
|
357
409
|
// lane history every turn.
|
|
358
410
|
const resumeId = this.statelessPerWake ? undefined : nextCtx.claudeSessionId;
|
|
@@ -379,7 +431,7 @@ export class ClaudeSdkAgent {
|
|
|
379
431
|
sessionId = message.session_id;
|
|
380
432
|
}
|
|
381
433
|
// Surface the model's own prose so a hired Claude agent is as legible as
|
|
382
|
-
// an FSM agent
|
|
434
|
+
// an FSM agent. Each completed assistant turn's text is recorded
|
|
383
435
|
// as a `thought` — the SAME agent-private channel the FSM uses for its
|
|
384
436
|
// reasoning (runTurn `emitThought` → record-event → recordThought), so it
|
|
385
437
|
// shows in the hired-agent view without cluttering the counterparty chat.
|
|
@@ -407,12 +459,23 @@ export class ClaudeSdkAgent {
|
|
|
407
459
|
if (message.type === 'result' && message.subtype !== 'success') {
|
|
408
460
|
runtimeLog.warn('ClaudeSdkAgent', `query ended without success subtype=${message.subtype} session=${identity.sessionId}`);
|
|
409
461
|
}
|
|
410
|
-
// Runaway guard
|
|
411
|
-
//
|
|
462
|
+
// Runaway guard: abort before an unbounded loop drains the
|
|
463
|
+
// key. Breaking the for-await calls the generator's return(),
|
|
412
464
|
// which terminates the underlying query.
|
|
413
|
-
|
|
465
|
+
const turnUsage = usageOf(message);
|
|
466
|
+
spentInput += costWeightedInput(turnUsage);
|
|
467
|
+
peakTurnInput = Math.max(peakTurnInput, billableInput(turnUsage));
|
|
468
|
+
// A wake that produced no per-turn usage still reports a cumulative
|
|
469
|
+
// total on the result message. Divided by the turns it covers, that is
|
|
470
|
+
// the average context per turn — a coarser read of the same transcript,
|
|
471
|
+
// used only when the precise one is unavailable.
|
|
472
|
+
if (message.type === 'result') {
|
|
473
|
+
const turns = Math.max(1, message.num_turns ?? 1);
|
|
474
|
+
peakTurnInput = Math.max(peakTurnInput, billableInput(message.usage) / turns);
|
|
475
|
+
}
|
|
414
476
|
if (spentInput > MAX_TASK_INPUT_TOKENS) {
|
|
415
|
-
|
|
477
|
+
abortedBy = 'input-budget';
|
|
478
|
+
runtimeLog.warn('ClaudeSdkAgent', `input-token budget exceeded (~${Math.round(spentInput)} cost-weighted > ${MAX_TASK_INPUT_TOKENS}) session=${identity.sessionId} agent=${this.agentId} — aborting query to cap spend`);
|
|
416
479
|
break;
|
|
417
480
|
}
|
|
418
481
|
}
|
|
@@ -421,11 +484,35 @@ export class ClaudeSdkAgent {
|
|
|
421
484
|
signal?.removeEventListener('abort', onAbort);
|
|
422
485
|
}
|
|
423
486
|
};
|
|
487
|
+
/**
|
|
488
|
+
* Say, on the agent's own reasoning channel, that this wake stopped before
|
|
489
|
+
* it finished. Without the note a wake cut off by a guard is
|
|
490
|
+
* indistinguishable from one that delivered: the task stays `active` with
|
|
491
|
+
* `processing: false`, no artifact, no result, and nothing anywhere says
|
|
492
|
+
* why. The lane's history carries the reason instead, so a wedged-task
|
|
493
|
+
* detector has something to read.
|
|
494
|
+
*/
|
|
495
|
+
const noteWakeCutShort = async (reason) => {
|
|
496
|
+
const text = `Wake stopped early: ${reason}. Work in progress was NOT filed — no ` +
|
|
497
|
+
`artifact was recorded and no task was closed by this wake.`;
|
|
498
|
+
await eff({
|
|
499
|
+
kind: 'record-event',
|
|
500
|
+
sessionId: identity.sessionId,
|
|
501
|
+
entry: { kind: 'thought', text },
|
|
502
|
+
idempotencyKey: writeKeys.artifactKey({ kind: 'thought', text }),
|
|
503
|
+
}).catch((err) => runtimeLog.warn('ClaudeSdkAgent', `record abort note failed session=${identity.sessionId}: ${err.message}`));
|
|
504
|
+
};
|
|
424
505
|
try {
|
|
425
506
|
await consumeQuery(resumeId);
|
|
426
507
|
}
|
|
427
508
|
catch (err) {
|
|
428
|
-
//
|
|
509
|
+
// Turn exhaustion arrives as a thrown error result, not a break, so it
|
|
510
|
+
// needs its own note before the throw propagates.
|
|
511
|
+
if (isMaxTurnsError(err)) {
|
|
512
|
+
abortedBy = 'max-turns';
|
|
513
|
+
await noteWakeCutShort(`the turn cap (${this.maxTurns}) was reached`);
|
|
514
|
+
}
|
|
515
|
+
// the resumed transcript is gone. Claude Code stores transcripts
|
|
429
516
|
// on the container's own disk while the session id outlives it — persisted
|
|
430
517
|
// per lane in the session store with a 24h TTL — so a redeployed agents
|
|
431
518
|
// task inherits an id pointing at a file that no longer exists. Every wake
|
|
@@ -442,9 +529,15 @@ export class ClaudeSdkAgent {
|
|
|
442
529
|
throw err;
|
|
443
530
|
}
|
|
444
531
|
runtimeLog.info('ClaudeSdkAgent', `resumed Claude session ${resumeId} no longer exists (container replaced?) ` +
|
|
445
|
-
`session=${identity.sessionId} agent=${this.agentId} — retrying once on a fresh session
|
|
532
|
+
`session=${identity.sessionId} agent=${this.agentId} — retrying once on a fresh session`);
|
|
446
533
|
nextCtx.claudeSessionId = undefined;
|
|
447
534
|
sessionId = undefined;
|
|
535
|
+
// The first attempt failed before any turn ran, so it spent nothing and
|
|
536
|
+
// left no transcript. Carrying its counters into the retry would halve the
|
|
537
|
+
// budget the real attempt gets to work with.
|
|
538
|
+
spentInput = 0;
|
|
539
|
+
peakTurnInput = 0;
|
|
540
|
+
abortedBy = null;
|
|
448
541
|
try {
|
|
449
542
|
await consumeQuery(undefined);
|
|
450
543
|
}
|
|
@@ -454,23 +547,31 @@ export class ClaudeSdkAgent {
|
|
|
454
547
|
}
|
|
455
548
|
}
|
|
456
549
|
// Stateless-per-wake keeps no session id — clear any carried over from the
|
|
457
|
-
// prior ctx so the next wake starts fresh too
|
|
550
|
+
// prior ctx so the next wake starts fresh too. Otherwise remember
|
|
458
551
|
// this wake's session for resume.
|
|
459
552
|
if (this.statelessPerWake)
|
|
460
553
|
nextCtx.claudeSessionId = undefined;
|
|
461
554
|
else if (sessionId)
|
|
462
555
|
nextCtx.claudeSessionId = sessionId;
|
|
463
|
-
//
|
|
464
|
-
//
|
|
465
|
-
//
|
|
466
|
-
//
|
|
467
|
-
//
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
556
|
+
// bound resumed-transcript growth. Past the ceiling, forget the session so
|
|
557
|
+
// the next wake starts fresh rather than paying an ever-larger replay.
|
|
558
|
+
//
|
|
559
|
+
// Measured as the largest SINGLE turn's input — that is the transcript,
|
|
560
|
+
// since every turn re-reads the whole thing. Summing the wake's turns
|
|
561
|
+
// instead measures how much WORK a wake did, not how much it carries: a lane
|
|
562
|
+
// whose transcript never leaves ~40k crosses the ceiling on turn five and
|
|
563
|
+
// drops its session on every wake. Resuming then never happens, so each wake
|
|
564
|
+
// re-reads the world from scratch and runs out of budget at the same step,
|
|
565
|
+
// and a standing job cannot advance.
|
|
566
|
+
if (nextCtx.claudeSessionId && peakTurnInput > MAX_SESSION_RESUME_INPUT_TOKENS) {
|
|
567
|
+
runtimeLog.info('ClaudeSdkAgent', `resetting Claude session (transcript ~${peakTurnInput} > ${MAX_SESSION_RESUME_INPUT_TOKENS}) ` +
|
|
568
|
+
`session=${identity.sessionId} agent=${this.agentId} — next wake starts fresh`);
|
|
472
569
|
nextCtx.claudeSessionId = undefined;
|
|
473
570
|
}
|
|
571
|
+
if (abortedBy === 'input-budget') {
|
|
572
|
+
await noteWakeCutShort(`the per-wake spend guard cut this run off (~${Math.round(spentInput)} ` +
|
|
573
|
+
`cost-weighted input tokens > ${MAX_TASK_INPUT_TOKENS})`);
|
|
574
|
+
}
|
|
474
575
|
return {
|
|
475
576
|
ctx: nextCtx,
|
|
476
577
|
state: state ?? DEFAULT_STATE,
|