@ziggs-ai/agent-sdk 0.16.1 → 0.17.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.
Files changed (113) hide show
  1. package/CHANGELOG.md +10 -0
  2. package/README.md +110 -88
  3. package/dist/{AgentHost.d.ts → Agent.d.ts} +50 -27
  4. package/dist/{AgentHost.js → Agent.js} +135 -129
  5. package/dist/adapters/AnthropicAdapter.d.ts +1 -1
  6. package/dist/adapters/AnthropicAdapter.js +1 -1
  7. package/dist/{agent/ClaudeSdkAgent.d.ts → brain/ClaudeBrain.d.ts} +23 -11
  8. package/dist/{agent/ClaudeSdkAgent.js → brain/ClaudeBrain.js} +99 -99
  9. package/dist/{agent/Agent.d.ts → brain/ZiggsBrain.d.ts} +23 -20
  10. package/dist/{agent/Agent.js → brain/ZiggsBrain.js} +12 -11
  11. package/dist/{agent → brain}/claudeCognition.d.ts +1 -1
  12. package/dist/{agent → brain}/claudeCognition.js +1 -1
  13. package/dist/{agent → brain}/claudeSdkProtocol.d.ts +1 -1
  14. package/dist/{agent → brain}/claudeSdkProtocol.js +1 -1
  15. package/dist/{agent → brain}/claudeSdkTools.d.ts +1 -1
  16. package/dist/{agent → brain}/claudeSdkTools.js +5 -14
  17. package/dist/{agent → brain}/hostedAgentConnection.d.ts +1 -1
  18. package/dist/{agent → brain}/hostedAgentConnection.js +3 -3
  19. package/dist/brain/index.d.ts +6 -6
  20. package/dist/brain/index.js +2 -2
  21. package/dist/brain/onBehalfAgreement.d.ts +51 -0
  22. package/dist/brain/onBehalfAgreement.js +99 -0
  23. package/dist/{agent → brain}/toolManagerToSdkMcp.js +10 -1
  24. package/dist/defineAgent.js +4 -4
  25. package/dist/formatters/HistoryFormatter.js +2 -1
  26. package/dist/index.d.ts +30 -31
  27. package/dist/index.js +24 -25
  28. package/dist/{adapters/McpAdapter.d.ts → mcp/createMcpAgent.d.ts} +16 -15
  29. package/dist/{adapters/McpAdapter.js → mcp/createMcpAgent.js} +54 -40
  30. package/dist/memory/MemoryStore.d.ts +18 -1
  31. package/dist/memory/MemoryStore.js +43 -0
  32. package/dist/{server → platform}/ConnectionPool.d.ts +9 -3
  33. package/dist/{server → platform}/ConnectionPool.js +16 -4
  34. package/dist/{server → platform}/EventQueue.d.ts +1 -1
  35. package/dist/{server → platform}/StateTimers.d.ts +1 -1
  36. package/dist/{server → platform}/StateTimers.js +1 -1
  37. package/dist/{server → platform}/ZiggsEffectHandler.d.ts +4 -4
  38. package/dist/{server → platform}/ZiggsEffectHandler.js +2 -2
  39. package/dist/{server → platform}/inboxLoop.d.ts +34 -1
  40. package/dist/{server → platform}/inboxLoop.js +122 -14
  41. package/dist/{server → platform}/polls/PollKind.d.ts +4 -7
  42. package/dist/{server → platform}/polls/PollKind.js +3 -6
  43. package/dist/{server → platform}/polls/PollRunner.d.ts +5 -6
  44. package/dist/{server → platform}/polls/PollRunner.js +24 -12
  45. package/dist/platform/polls/declaredPollRunner.d.ts +4 -0
  46. package/dist/platform/polls/declaredPollRunner.js +29 -0
  47. package/dist/{server → platform}/tools/tier2/mcpConnectionTools.js +6 -3
  48. package/dist/runtime/bootstrapCtx.d.ts +1 -1
  49. package/dist/runtime/bootstrapCtx.js +1 -1
  50. package/dist/runtime/buildOutcome.js +1 -1
  51. package/dist/runtime/runTurn.d.ts +1 -1
  52. package/dist/runtime/runTurn.js +18 -18
  53. package/dist/runtime/validateWorkflow.js +1 -1
  54. package/dist/skills/parseSkill.d.ts +7 -2
  55. package/dist/skills/parseSkill.js +22 -5
  56. package/dist/skills/skillReadReference.d.ts +6 -0
  57. package/dist/skills/skillReadReference.js +27 -0
  58. package/dist/tools/needs.js +2 -2
  59. package/dist/tools/toolCreds.d.ts +1 -1
  60. package/dist/tools/toolCreds.js +1 -1
  61. package/dist/types.d.ts +3 -3
  62. package/package.json +2 -2
  63. package/dist/server/createHealthServer.d.ts +0 -7
  64. package/dist/server/createHealthServer.js +0 -75
  65. package/dist/server/runLauncher.d.ts +0 -22
  66. package/dist/server/runLauncher.js +0 -160
  67. /package/dist/{agent → brain}/toolManagerToSdkMcp.d.ts +0 -0
  68. /package/dist/{server → platform}/EventQueue.js +0 -0
  69. /package/dist/{server → platform}/OutboxBuffer.d.ts +0 -0
  70. /package/dist/{server → platform}/OutboxBuffer.js +0 -0
  71. /package/dist/{server → platform}/SeenMessages.d.ts +0 -0
  72. /package/dist/{server → platform}/SeenMessages.js +0 -0
  73. /package/dist/{server → platform}/agreements/AgreementService.d.ts +0 -0
  74. /package/dist/{server → platform}/agreements/AgreementService.js +0 -0
  75. /package/dist/{server → platform}/proactive/ProactiveTrigger.d.ts +0 -0
  76. /package/dist/{server → platform}/proactive/ProactiveTrigger.js +0 -0
  77. /package/dist/{server → platform}/requestMatch.d.ts +0 -0
  78. /package/dist/{server → platform}/requestMatch.js +0 -0
  79. /package/dist/{server → platform}/tasks/TaskService.d.ts +0 -0
  80. /package/dist/{server → platform}/tasks/TaskService.js +0 -0
  81. /package/dist/{server → platform}/tasks/index.d.ts +0 -0
  82. /package/dist/{server → platform}/tasks/index.js +0 -0
  83. /package/dist/{server → platform}/telemetryIngest.d.ts +0 -0
  84. /package/dist/{server → platform}/telemetryIngest.js +0 -0
  85. /package/dist/{server → platform}/tools/index.d.ts +0 -0
  86. /package/dist/{server → platform}/tools/index.js +0 -0
  87. /package/dist/{server → platform}/tools/tier1/protocolRunner.d.ts +0 -0
  88. /package/dist/{server → platform}/tools/tier1/protocolRunner.js +0 -0
  89. /package/dist/{server → platform}/tools/tier1/protocolTools.d.ts +0 -0
  90. /package/dist/{server → platform}/tools/tier1/protocolTools.js +0 -0
  91. /package/dist/{server → platform}/tools/tier2/artifactTools.d.ts +0 -0
  92. /package/dist/{server → platform}/tools/tier2/artifactTools.js +0 -0
  93. /package/dist/{server → platform}/tools/tier2/chatTools.d.ts +0 -0
  94. /package/dist/{server → platform}/tools/tier2/chatTools.js +0 -0
  95. /package/dist/{server → platform}/tools/tier2/connectionTools.d.ts +0 -0
  96. /package/dist/{server → platform}/tools/tier2/connectionTools.js +0 -0
  97. /package/dist/{server → platform}/tools/tier2/contextTools.d.ts +0 -0
  98. /package/dist/{server → platform}/tools/tier2/contextTools.js +0 -0
  99. /package/dist/{server → platform}/tools/tier2/discoveryTools.d.ts +0 -0
  100. /package/dist/{server → platform}/tools/tier2/discoveryTools.js +0 -0
  101. /package/dist/{server → platform}/tools/tier2/grantsTools.d.ts +0 -0
  102. /package/dist/{server → platform}/tools/tier2/grantsTools.js +0 -0
  103. /package/dist/{server → platform}/tools/tier2/introductionTools.d.ts +0 -0
  104. /package/dist/{server → platform}/tools/tier2/introductionTools.js +0 -0
  105. /package/dist/{server → platform}/tools/tier2/linkTools.d.ts +0 -0
  106. /package/dist/{server → platform}/tools/tier2/linkTools.js +0 -0
  107. /package/dist/{server → platform}/tools/tier2/marketplaceTools.d.ts +0 -0
  108. /package/dist/{server → platform}/tools/tier2/marketplaceTools.js +0 -0
  109. /package/dist/{server → platform}/tools/tier2/mcpConnectionTools.d.ts +0 -0
  110. /package/dist/{server → platform}/tools/tier2/paymentTools.d.ts +0 -0
  111. /package/dist/{server → platform}/tools/tier2/paymentTools.js +0 -0
  112. /package/dist/{server → platform}/tools/tier2/taskTools.d.ts +0 -0
  113. /package/dist/{server → platform}/tools/tier2/taskTools.js +0 -0
@@ -1,8 +1,9 @@
1
+ import type { Brain } from './index.js';
1
2
  import { query } from '@anthropic-ai/claude-agent-sdk';
2
3
  import type { Workflow } from '../types.js';
3
4
  import type { ToolManager } from '../tools/ToolManager.js';
4
5
  import { type RawEvent } from '../runtime/buildOutcome.js';
5
- import type { TickInput, TickOutput } from './Agent.js';
6
+ import type { TickInput, TickOutput } from './ZiggsBrain.js';
6
7
  /**
7
8
  * status notices must not spend a Claude turn answering
8
9
  * nobody. Real work wakes arrive as chat `message`, synthetic `task_result`
@@ -21,9 +22,15 @@ import type { TickInput, TickOutput } from './Agent.js';
21
22
  * message still lands in chat history and arrives as context on the next
22
23
  * real wake — skipped here is the LLM turn, not the information.
23
24
  */
25
+ /**
26
+ * Store publish chats the agent in a room named `probe_<agentId>_<ts>`.
27
+ * That check is "are you connected and can you reply", not a hired job —
28
+ * spending a model turn on it is how a long skill prompt 504s the gate.
29
+ */
30
+ export declare function isCapabilityProbeSession(sessionId: string | undefined | null): boolean;
24
31
  export declare function isNoticeOnlyWake(event: RawEvent | undefined, ownAgentId?: string | null): boolean;
25
32
  export type CognitionKind = 'fsm' | 'claude-sdk';
26
- export interface ClaudeSdkAgentOptions {
33
+ export interface ClaudeBrainOptions {
27
34
  workflow: Workflow;
28
35
  toolManager: ToolManager;
29
36
  /** Store-facing one-liner. Used as the role block only when `systemPrompt` is absent. */
@@ -63,15 +70,18 @@ export interface ClaudeSdkAgentOptions {
63
70
  export declare function isMaxTurnsError(err: unknown): boolean;
64
71
  export declare function isMissingSessionError(err: unknown): boolean;
65
72
  /**
66
- * A Ziggs agent whose brain is the Claude Agent SDK. Same host, same `eff`
67
- * side channel, same tool surface as the FSM `Agent` — the ONLY difference is
73
+ * A brain driven by the Claude Agent SDK. Same body, same `eff`
74
+ * side channel, same tool surface as `ZiggsBrain` — the ONLY difference is
68
75
  * the loop: instead of the FSM + `runTurn`, a wake drives a headless Claude
69
76
  * `query()` (see "host is the product; the brain is how it decides").
70
77
  */
71
- export declare class ClaudeSdkAgent {
78
+ export declare class ClaudeBrain implements Brain {
79
+ readonly kind: "claude";
72
80
  workflow: Workflow;
73
81
  toolManager: ToolManager;
74
82
  private rolePrompt;
83
+ /** Store one-liner — used when the publish probe asks who we are. */
84
+ private listingBlurb;
75
85
  private specialization;
76
86
  private model;
77
87
  private anthropicKey;
@@ -81,12 +91,14 @@ export declare class ClaudeSdkAgent {
81
91
  private statelessPerWake;
82
92
  private queryFn;
83
93
  /**
84
- * #7 — tool set is fixed at host construction; rebuilding the
85
- * in-process MCP server (+ zod schema tree) every wake was pure waste.
86
- * A mutable holder supplies per-wake context without closing over `tick`.
94
+ * Per-lane tool context. One agent object serves every room, and a
95
+ * wake's query can still be running when another lane starts. A single
96
+ * `current` field meant B's room/sender/write keys overwrote A's
97
+ * mid-query. Wakes on one lane stay serialized; one server per lane is
98
+ * enough, and getContext closes over that lane's key.
87
99
  */
88
- private toolContextHolder;
89
- private sdkMcpServer;
90
- constructor(opts: ClaudeSdkAgentOptions);
100
+ private toolContextByLane;
101
+ private sdkMcpByLane;
102
+ constructor(opts: ClaudeBrainOptions);
91
103
  tick({ event, ctx, state, identity, eff, signal }: TickInput): Promise<TickOutput>;
92
104
  }
@@ -1,3 +1,4 @@
1
+ import { isAgreementId } from '@ziggs-ai/api-client';
1
2
  import { query } from '@anthropic-ai/claude-agent-sdk';
2
3
  import { isMessageAddressedElsewhere, outcomeFromEvent } from '../runtime/buildOutcome.js';
3
4
  import { getBatchEvents } from '../context/batch.js';
@@ -8,6 +9,7 @@ import { buildZiggsSdkMcpServer } from './toolManagerToSdkMcp.js';
8
9
  import { claudeQueryOptions, buildClaudeEventPrompt, summarizeContext, extractAssistantText, } from './claudeCognition.js';
9
10
  import { assertHostedAgentZiggsTools } from './hostedAgentConnection.js';
10
11
  import { runtimeLog } from '../shared/runtimeLog.js';
12
+ import { resolveOnBehalfAgreementId, } from './onBehalfAgreement.js';
11
13
  /**
12
14
  * status notices must not spend a Claude turn answering
13
15
  * nobody. Real work wakes arrive as chat `message`, synthetic `task_result`
@@ -26,6 +28,14 @@ import { runtimeLog } from '../shared/runtimeLog.js';
26
28
  * message still lands in chat history and arrives as context on the next
27
29
  * real wake — skipped here is the LLM turn, not the information.
28
30
  */
31
+ /**
32
+ * Store publish chats the agent in a room named `probe_<agentId>_<ts>`.
33
+ * That check is "are you connected and can you reply", not a hired job —
34
+ * spending a model turn on it is how a long skill prompt 504s the gate.
35
+ */
36
+ export function isCapabilityProbeSession(sessionId) {
37
+ return typeof sessionId === 'string' && sessionId.startsWith('probe_');
38
+ }
29
39
  export function isNoticeOnlyWake(event, ownAgentId) {
30
40
  const events = getBatchEvents(event);
31
41
  if (events.length === 0)
@@ -164,66 +174,18 @@ function extractMetrics(message) {
164
174
  };
165
175
  }
166
176
  /**
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
169
- * `senderId` — which, under a delegation, is the delegating agent (who owns no
170
- * connection) → a spurious NO_USER_CONNECTION even though the payer IS connected.
171
- *
172
- * The id must be found on ANY wake the worker might fetch on, not just
173
- * `task_result` events. In practice the worker often fetches while reacting to
174
- * the proposal-approved / agreement-active lifecycle events, which carry the id
175
- * at the event top level rather than under `result`. Reading only
176
- * `result.agreementId` (the original wiring) left those wakes with no
177
- * agreement → the false "authorize your connection" prompt. Resolve from every
178
- * reliable source instead:
179
- * 1. task_result events → `result.agreementId`
180
- * 2. agreement-lifecycle events → top-level `agreementId`
181
- * 3. task lanes → `sessionId` IS the agreement id
182
- * 4. fallback → the single active agreement in context this agent provides
183
- */
184
- function resolveOnBehalfAgreementId(rawEvent, contextSnap, agentId, sessionId) {
185
- const str = (v) => typeof v === 'string' && v ? v : undefined;
186
- const fromResult = str(rawEvent?.result?.agreementId);
187
- if (fromResult)
188
- return fromResult;
189
- const fromEvent = str(rawEvent?.agreementId);
190
- if (fromEvent)
191
- return fromEvent;
192
- if (typeof sessionId === 'string' && sessionId.startsWith('agr_'))
193
- return sessionId;
194
- // Fallback: exactly one active agreement in context where this agent is the
195
- // provider. Guarded to a single match so we never send the wrong agreement.
196
- const ags = Array.isArray(contextSnap?.agreements)
197
- ? contextSnap.agreements
198
- : [];
199
- const provides = ags.filter((a) => {
200
- const provider = a?.parties?.provider;
201
- const providerIds = [provider?.actor, a?.providerId, a?.providerAgentId, a?.executorId];
202
- const isProvider = providerIds.some((x) => x === agentId);
203
- const status = a?.status != null ? String(a.status).toLowerCase() : '';
204
- if (status === 'cancelled' ||
205
- status === 'refunded' ||
206
- status === 'fulfilled') {
207
- return false;
208
- }
209
- const active = status === 'active' || status === 'approved';
210
- return isProvider && active;
211
- });
212
- if (provides.length === 1) {
213
- return str(provides[0].agreementId) ?? str(provides[0].id) ?? str(provides[0]._id);
214
- }
215
- return undefined;
216
- }
217
- /**
218
- * A Ziggs agent whose brain is the Claude Agent SDK. Same host, same `eff`
219
- * side channel, same tool surface as the FSM `Agent` — the ONLY difference is
177
+ * A brain driven by the Claude Agent SDK. Same body, same `eff`
178
+ * side channel, same tool surface as `ZiggsBrain` — the ONLY difference is
220
179
  * the loop: instead of the FSM + `runTurn`, a wake drives a headless Claude
221
180
  * `query()` (see "host is the product; the brain is how it decides").
222
181
  */
223
- export class ClaudeSdkAgent {
182
+ export class ClaudeBrain {
183
+ kind = 'claude';
224
184
  workflow;
225
185
  toolManager;
226
186
  rolePrompt;
187
+ /** Store one-liner — used when the publish probe asks who we are. */
188
+ listingBlurb;
227
189
  specialization;
228
190
  model;
229
191
  anthropicKey;
@@ -233,25 +195,28 @@ export class ClaudeSdkAgent {
233
195
  statelessPerWake;
234
196
  queryFn;
235
197
  /**
236
- * #7 — tool set is fixed at host construction; rebuilding the
237
- * in-process MCP server (+ zod schema tree) every wake was pure waste.
238
- * A mutable holder supplies per-wake context without closing over `tick`.
198
+ * Per-lane tool context. One agent object serves every room, and a
199
+ * wake's query can still be running when another lane starts. A single
200
+ * `current` field meant B's room/sender/write keys overwrote A's
201
+ * mid-query. Wakes on one lane stay serialized; one server per lane is
202
+ * enough, and getContext closes over that lane's key.
239
203
  */
240
- toolContextHolder = { current: null };
241
- sdkMcpServer = null;
204
+ toolContextByLane = new Map();
205
+ sdkMcpByLane = new Map();
242
206
  constructor(opts) {
243
207
  if (!opts.workflow?.states)
244
- throw new Error('ClaudeSdkAgent: workflow with states is required');
208
+ throw new Error('ClaudeBrain: workflow with states is required');
245
209
  if (!opts.toolManager)
246
- throw new Error('ClaudeSdkAgent: toolManager is required');
210
+ throw new Error('ClaudeBrain: toolManager is required');
247
211
  if (!opts.anthropicKey)
248
- throw new Error('ClaudeSdkAgent: anthropicKey is required');
212
+ throw new Error('ClaudeBrain: anthropicKey is required');
249
213
  this.workflow = opts.workflow;
250
214
  this.toolManager = opts.toolManager;
251
215
  // the prompt and the store blurb are two different strings. Agents
252
216
  // written before the split still carry the prompt in `description`, so fall
253
217
  // back to it rather than silently dropping their role.
254
218
  this.rolePrompt = opts.systemPrompt || opts.description;
219
+ this.listingBlurb = opts.description;
255
220
  this.specialization = opts.specialization ?? null;
256
221
  this.model = opts.model;
257
222
  this.anthropicKey = opts.anthropicKey;
@@ -263,18 +228,15 @@ export class ClaudeSdkAgent {
263
228
  }
264
229
  async tick({ event, ctx, state, identity, eff, signal }) {
265
230
  if (!identity?.sessionId)
266
- throw new Error('ClaudeSdkAgent.tick: identity.sessionId is required');
231
+ throw new Error('ClaudeBrain.tick: identity.sessionId is required');
267
232
  if (!eff)
268
- throw new Error('ClaudeSdkAgent.tick: eff is required');
233
+ throw new Error('ClaudeBrain.tick: eff is required');
269
234
  const rawEvent = event;
270
235
  const nextCtx = bootstrapCtx(identity, ctx);
271
236
  nextCtx.lastOutcome = outcomeFromEvent(rawEvent ?? null, identity.agentId);
272
- const now = Date.now();
273
- const recentDeliveries = (nextCtx.recentDeliveries ?? []).filter((t) => now - t < WAKE_WINDOW_MS);
274
- if (recentDeliveries.length >= MAX_WAKES_PER_WINDOW) {
275
- runtimeLog.warn('ClaudeSdkAgent', `wake-rate breaker tripped: ${recentDeliveries.length} wakes in ${WAKE_WINDOW_MS / 60_000}min ` +
276
- `session=${identity.sessionId} agent=${this.agentId} — skipping LLM for this wake`);
277
- nextCtx.recentDeliveries = recentDeliveries;
237
+ if (isNoticeOnlyWake(rawEvent, identity.agentId)) {
238
+ runtimeLog.info('ClaudeBrain', `notice-only wake — skipping LLM session=${identity.sessionId} agent=${this.agentId} ` +
239
+ `type=${rawEvent?.type ?? 'unknown'}`);
278
240
  return {
279
241
  ctx: nextCtx,
280
242
  state: state ?? DEFAULT_STATE,
@@ -283,10 +245,42 @@ export class ClaudeSdkAgent {
283
245
  metrics: { llmCalls: 0, tokens: { total: 0, input: 0, output: 0 } },
284
246
  };
285
247
  }
286
- nextCtx.recentDeliveries = [...recentDeliveries, now];
287
- if (isNoticeOnlyWake(rawEvent, identity.agentId)) {
288
- runtimeLog.info('ClaudeSdkAgent', `notice-only wake — skipping LLM session=${identity.sessionId} agent=${this.agentId} ` +
289
- `type=${rawEvent?.type ?? 'unknown'}`);
248
+ // Store publish must hear a reply even when leftover mail already tripped
249
+ // the wake-rate breaker. The breaker used to sit above this and park the
250
+ // probe with no send-message — Register then died on ALB 504.
251
+ if (nextCtx.lastOutcome?.kind === 'message-received' &&
252
+ isCapabilityProbeSession(identity.sessionId)) {
253
+ const text = `I received your message. ${this.listingBlurb.trim().split(/\r?\n/)[0]?.trim() || 'I am online.'}`;
254
+ const writeKeys = createDeliveryWriteKeys(identity.laneKey, rawEvent);
255
+ const senderId = rawEvent?.senderId != null && String(rawEvent.senderId).trim() !== ''
256
+ ? String(rawEvent.senderId)
257
+ : undefined;
258
+ await eff({
259
+ kind: 'send-message',
260
+ sessionId: identity.sessionId,
261
+ ...(senderId ? { receiverId: senderId } : {}),
262
+ text,
263
+ messageId: writeKeys.messageKey({
264
+ text,
265
+ sessionId: identity.sessionId,
266
+ receiverId: senderId,
267
+ }),
268
+ });
269
+ runtimeLog.info('ClaudeBrain', `capability-probe reply — skipping LLM session=${identity.sessionId} agent=${this.agentId}`);
270
+ return {
271
+ ctx: nextCtx,
272
+ state: state ?? DEFAULT_STATE,
273
+ parked: true,
274
+ stateTimeoutUnhandled: false,
275
+ metrics: { llmCalls: 0, tokens: { total: 0, input: 0, output: 0 } },
276
+ };
277
+ }
278
+ const now = Date.now();
279
+ const recentDeliveries = (nextCtx.recentDeliveries ?? []).filter((t) => now - t < WAKE_WINDOW_MS);
280
+ if (recentDeliveries.length >= MAX_WAKES_PER_WINDOW) {
281
+ runtimeLog.warn('ClaudeBrain', `wake-rate breaker tripped: ${recentDeliveries.length} wakes in ${WAKE_WINDOW_MS / 60_000}min ` +
282
+ `session=${identity.sessionId} agent=${this.agentId} — skipping LLM for this wake`);
283
+ nextCtx.recentDeliveries = recentDeliveries;
290
284
  return {
291
285
  ctx: nextCtx,
292
286
  state: state ?? DEFAULT_STATE,
@@ -295,6 +289,7 @@ export class ClaudeSdkAgent {
295
289
  metrics: { llmCalls: 0, tokens: { total: 0, input: 0, output: 0 } },
296
290
  };
297
291
  }
292
+ nextCtx.recentDeliveries = [...recentDeliveries, now];
298
293
  let contextSnap = null;
299
294
  try {
300
295
  // #5: pre-read is for wake gating + summarizeContext (a count) +
@@ -307,7 +302,7 @@ export class ClaudeSdkAgent {
307
302
  });
308
303
  }
309
304
  catch (err) {
310
- runtimeLog.warn('ClaudeSdkAgent', `read-context failed session=${identity.sessionId}: ${err.message}`);
305
+ runtimeLog.warn('ClaudeBrain', `read-context failed session=${identity.sessionId}: ${err.message}`);
311
306
  }
312
307
  // Same guard as the FSM's runTurn: a message wake for a chat the backend
313
308
  // no longer resolves (deleted / not visible) has no world to act in —
@@ -319,17 +314,15 @@ export class ClaudeSdkAgent {
319
314
  // signal. Loud-log that mis-key; message wakes still skip below.
320
315
  if (contextSnap?.chatMissing === true) {
321
316
  const sessionId = identity.sessionId;
322
- const looksLikeBareAgreement = typeof sessionId === 'string' &&
323
- sessionId.startsWith('agr_') &&
324
- !sessionId.startsWith('agrn-');
317
+ const looksLikeBareAgreement = isAgreementId(sessionId);
325
318
  if (looksLikeBareAgreement) {
326
- runtimeLog.warn('ClaudeSdkAgent', `chatMissing on non-chat sessionId=${sessionId} agent=${this.agentId} ` +
319
+ runtimeLog.warn('ClaudeBrain', `chatMissing on non-chat sessionId=${sessionId} agent=${this.agentId} ` +
327
320
  `outcome=${nextCtx.lastOutcome?.kind ?? 'none'} — lane was keyed by ` +
328
321
  `agreementId instead of the agreement's origin/notification chat; ` +
329
322
  `wake continues with an empty snapshot`);
330
323
  }
331
324
  if (nextCtx.lastOutcome?.kind === 'message-received') {
332
- runtimeLog.warn('ClaudeSdkAgent', `chat ${sessionId} not found on backend — skipping message wake agent=${this.agentId}`);
325
+ runtimeLog.warn('ClaudeBrain', `chat ${sessionId} not found on backend — skipping message wake agent=${this.agentId}`);
333
326
  return {
334
327
  ctx: nextCtx,
335
328
  state: state ?? DEFAULT_STATE,
@@ -364,26 +357,33 @@ export class ClaudeSdkAgent {
364
357
  // MCP call resolve the originating end-user (payer) rather than the
365
358
  // delegating agent (senderId). Resolved from every reliable wire source,
366
359
  // not just task_result events — see resolveOnBehalfAgreementId.
367
- onBehalfOfAgreementId: resolveOnBehalfAgreementId(rawEvent, contextSnap, this.agentId, identity.sessionId),
360
+ // Skip payer-only subs named on the worker result; bind to the
361
+ // provider hire (see onBehalfAgreement.ts).
362
+ onBehalfOfAgreementId: resolveOnBehalfAgreementId(rawEvent, Array.isArray(contextSnap?.agreements)
363
+ ? contextSnap.agreements
364
+ : null, this.agentId, identity.sessionId),
368
365
  runId,
369
366
  writeKeys,
370
367
  };
371
- this.toolContextHolder.current = toolContext;
372
- if (!this.sdkMcpServer) {
368
+ const lane = identity.laneKey;
369
+ this.toolContextByLane.set(lane, toolContext);
370
+ let sdkMcpServer = this.sdkMcpByLane.get(lane);
371
+ if (!sdkMcpServer) {
373
372
  const availableTools = await this.toolManager.getAvailableTools();
374
- this.sdkMcpServer = buildZiggsSdkMcpServer(availableTools, () => {
375
- const ctx = this.toolContextHolder.current;
373
+ sdkMcpServer = buildZiggsSdkMcpServer(availableTools, () => {
374
+ const ctx = this.toolContextByLane.get(lane);
376
375
  if (!ctx) {
377
- throw new Error('ClaudeSdkAgent: tool context missing for in-process MCP call');
376
+ throw new Error(`ClaudeBrain: tool context missing for lane ${lane}`);
378
377
  }
379
378
  return ctx;
380
379
  }, {
381
380
  name: 'ziggs',
382
381
  instructions: 'In-process Ziggs tools backed by api-client. No external MCP process.',
383
382
  });
383
+ this.sdkMcpByLane.set(lane, sdkMcpServer);
384
384
  }
385
385
  const prompt = buildClaudeEventPrompt(rawEvent, summarizeContext(contextSnap));
386
- const mcpServers = { ziggs: this.sdkMcpServer };
386
+ const mcpServers = { ziggs: sdkMcpServer };
387
387
  assertHostedAgentZiggsTools(mcpServers);
388
388
  const queryOptions = claudeQueryOptions({
389
389
  systemPrompt,
@@ -394,7 +394,7 @@ export class ClaudeSdkAgent {
394
394
  stderr: (data) => {
395
395
  const line = data.trim();
396
396
  if (line)
397
- runtimeLog.warn('ClaudeSdkAgent', `stderr session=${identity.sessionId}: ${line}`);
397
+ runtimeLog.warn('ClaudeBrain', `stderr session=${identity.sessionId}: ${line}`);
398
398
  },
399
399
  });
400
400
  let metrics = { llmCalls: 0, tokens: { total: 0, input: 0, output: 0, cacheRead: 0, cacheCreation: 0 } };
@@ -429,7 +429,7 @@ export class ClaudeSdkAgent {
429
429
  try {
430
430
  for await (const message of query) {
431
431
  if (signal?.aborted) {
432
- runtimeLog.warn('ClaudeSdkAgent', `host watchdog aborted wake session=${identity.sessionId} agent=${this.agentId} — stopping query`);
432
+ runtimeLog.warn('ClaudeBrain', `host watchdog aborted wake session=${identity.sessionId} agent=${this.agentId} — stopping query`);
433
433
  break;
434
434
  }
435
435
  if ('session_id' in message && typeof message.session_id === 'string') {
@@ -455,14 +455,14 @@ export class ClaudeSdkAgent {
455
455
  // reorder key assignment against other writes of this wake.
456
456
  const idempotencyKey = writeKeys.artifactKey({ kind: 'thought', text });
457
457
  void eff({ kind: 'record-event', sessionId: identity.sessionId, entry: { kind: 'thought', text }, idempotencyKey })
458
- .catch((err) => runtimeLog.warn('ClaudeSdkAgent', `record thought failed session=${identity.sessionId}: ${err.message}`));
458
+ .catch((err) => runtimeLog.warn('ClaudeBrain', `record thought failed session=${identity.sessionId}: ${err.message}`));
459
459
  }
460
460
  }
461
461
  const m = extractMetrics(message);
462
462
  if (m)
463
463
  metrics = m;
464
464
  if (message.type === 'result' && message.subtype !== 'success') {
465
- runtimeLog.warn('ClaudeSdkAgent', `query ended without success subtype=${message.subtype} session=${identity.sessionId}`);
465
+ runtimeLog.warn('ClaudeBrain', `query ended without success subtype=${message.subtype} session=${identity.sessionId}`);
466
466
  }
467
467
  // Runaway guard: abort before an unbounded loop drains the
468
468
  // key. Breaking the for-await calls the generator's return(),
@@ -480,7 +480,7 @@ export class ClaudeSdkAgent {
480
480
  }
481
481
  if (spentInput > MAX_TASK_INPUT_TOKENS) {
482
482
  abortedBy = 'input-budget';
483
- 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`);
483
+ runtimeLog.warn('ClaudeBrain', `input-token budget exceeded (~${Math.round(spentInput)} cost-weighted > ${MAX_TASK_INPUT_TOKENS}) session=${identity.sessionId} agent=${this.agentId} — aborting query to cap spend`);
484
484
  break;
485
485
  }
486
486
  }
@@ -531,9 +531,9 @@ export class ClaudeSdkAgent {
531
531
  status: 'failed',
532
532
  },
533
533
  },
534
- }).catch((err) => runtimeLog.warn('ClaudeSdkAgent', `could not fail task ${taskId} after a cut-short wake ` +
534
+ }).catch((err) => runtimeLog.warn('ClaudeBrain', `could not fail task ${taskId} after a cut-short wake ` +
535
535
  `session=${identity.sessionId}: ${err.message}`));
536
- runtimeLog.warn('ClaudeSdkAgent', `failed task ${taskId} because the wake was cut short session=${identity.sessionId} agent=${this.agentId}`);
536
+ runtimeLog.warn('ClaudeBrain', `failed task ${taskId} because the wake was cut short session=${identity.sessionId} agent=${this.agentId}`);
537
537
  };
538
538
  const noteWakeCutShort = async (reason) => {
539
539
  const text = `Wake stopped early: ${reason}. Work in progress was NOT filed — no ` +
@@ -543,7 +543,7 @@ export class ClaudeSdkAgent {
543
543
  sessionId: identity.sessionId,
544
544
  entry: { kind: 'thought', text },
545
545
  idempotencyKey: writeKeys.artifactKey({ kind: 'thought', text }),
546
- }).catch((err) => runtimeLog.warn('ClaudeSdkAgent', `record abort note failed session=${identity.sessionId}: ${err.message}`));
546
+ }).catch((err) => runtimeLog.warn('ClaudeBrain', `record abort note failed session=${identity.sessionId}: ${err.message}`));
547
547
  };
548
548
  try {
549
549
  await consumeQuery(resumeId);
@@ -573,10 +573,10 @@ export class ClaudeSdkAgent {
573
573
  // attempt — a mid-wake failure must keep propagating, since its partial
574
574
  // writes are already out.
575
575
  if (!isMissingSessionError(err) || !resumeId) {
576
- runtimeLog.warn('ClaudeSdkAgent', `query failed session=${identity.sessionId}: ${err.message}`);
576
+ runtimeLog.warn('ClaudeBrain', `query failed session=${identity.sessionId}: ${err.message}`);
577
577
  throw err;
578
578
  }
579
- runtimeLog.info('ClaudeSdkAgent', `resumed Claude session ${resumeId} no longer exists (container replaced?) ` +
579
+ runtimeLog.info('ClaudeBrain', `resumed Claude session ${resumeId} no longer exists (container replaced?) ` +
580
580
  `session=${identity.sessionId} agent=${this.agentId} — retrying once on a fresh session`);
581
581
  nextCtx.claudeSessionId = undefined;
582
582
  sessionId = undefined;
@@ -590,7 +590,7 @@ export class ClaudeSdkAgent {
590
590
  await consumeQuery(undefined);
591
591
  }
592
592
  catch (retryErr) {
593
- runtimeLog.warn('ClaudeSdkAgent', `query failed session=${identity.sessionId}: ${retryErr.message}`);
593
+ runtimeLog.warn('ClaudeBrain', `query failed session=${identity.sessionId}: ${retryErr.message}`);
594
594
  throw retryErr;
595
595
  }
596
596
  }
@@ -612,7 +612,7 @@ export class ClaudeSdkAgent {
612
612
  // re-reads the world from scratch and runs out of budget at the same step,
613
613
  // and a standing job cannot advance.
614
614
  if (nextCtx.claudeSessionId && peakTurnInput > MAX_SESSION_RESUME_INPUT_TOKENS) {
615
- runtimeLog.info('ClaudeSdkAgent', `resetting Claude session (transcript ~${peakTurnInput} > ${MAX_SESSION_RESUME_INPUT_TOKENS}) ` +
615
+ runtimeLog.info('ClaudeBrain', `resetting Claude session (transcript ~${peakTurnInput} > ${MAX_SESSION_RESUME_INPUT_TOKENS}) ` +
616
616
  `session=${identity.sessionId} agent=${this.agentId} — next wake starts fresh`);
617
617
  nextCtx.claudeSessionId = undefined;
618
618
  }
@@ -1,9 +1,10 @@
1
+ import type { Brain } from './index.js';
1
2
  import type { Ctx, Identity, Workflow, EffectHandler } from '../types.js';
2
3
  import { type TransitionEvent } from '../runtime/AgentMachine.js';
3
4
  import { PromptBuilder } from '../runtime/PromptBuilder.js';
4
5
  import type { MemoryStore } from '../memory/MemoryStore.js';
5
6
  import type { ToolManager } from '../tools/ToolManager.js';
6
- export interface AgentOptions {
7
+ export interface ZiggsBrainOptions {
7
8
  workflow: Workflow;
8
9
  toolManager: ToolManager;
9
10
  promptBuilder?: PromptBuilder;
@@ -11,26 +12,27 @@ export interface AgentOptions {
11
12
  export interface TickInput {
12
13
  /** Wire event (already normalized: a `message` or `task_result`-style envelope). */
13
14
  event: unknown;
14
- /** Server-provided per-session state. Omit on the first tick — a fresh ctx is constructed. */
15
+ /** Agent-provided per-session state. Omit on the first tick — a fresh ctx is constructed. */
15
16
  ctx?: Ctx;
16
- /** Server-provided workflow state name. Omit on the first tick — defaults to `workflow.initial`. */
17
+ /** Agent-provided workflow state name. Omit on the first tick — defaults to `workflow.initial`. */
17
18
  state?: string;
18
- /** Who am I and what session am I running in. The Agent carries this through Ctx. */
19
+ /** Who am I and what session am I running in. The ZiggsBrain carries this through Ctx. */
19
20
  identity: Identity;
20
21
  /** The single side channel for I/O — LLM, context, tools, send, record. */
21
22
  eff: EffectHandler;
22
23
  /**
23
24
  * Agent-private long-term memory. Operator-provided (default
24
- * `InMemoryStore` when omitted by `AgentHost`). Key shape is the agent's
25
- * choice — `counterparty:<id>`, `agreement:<id>:notes`, etc.
25
+ * `InMemoryStore` when omitted by `Agent`). The agent already scopes
26
+ * this to the hire the wake is inside; key shape inside that section is
27
+ * the agent's choice — `counterparty:<id>`, `agreement:<id>:notes`, etc.
26
28
  */
27
29
  memory?: MemoryStore;
28
30
  /** Optional tap for FSM transition events. */
29
31
  transitionTap?: (event: TransitionEvent) => void;
30
32
  /**
31
- * Aborted by the host when the tick watchdog fires. A brain driving an
32
- * external loop (the Claude Agent SDK query) MUST stop consuming it on
33
- * abort — the host's Promise.race only abandons the tick promise, so
33
+ * Aborted by the agent when the tick watchdog fires. A brain driving an
34
+ * external loop (the Claude ZiggsBrain SDK query) MUST stop consuming it on
35
+ * abort — the agent's Promise.race only abandons the tick promise, so
34
36
  * without this the query keeps running as a zombie: burning tokens and
35
37
  * racing its own redelivered retry (observed as duplicate kickoffs and a
36
38
  * 363k-token wake, 2026-07-26).
@@ -38,13 +40,13 @@ export interface TickInput {
38
40
  signal?: AbortSignal;
39
41
  }
40
42
  export interface TickOutput {
41
- /** Updated ctx — Server persists this against the session. */
43
+ /** Updated ctx — Agent persists this against the session. */
42
44
  ctx: Ctx;
43
- /** Workflow state name after the tick — Server persists this against the session. */
45
+ /** Workflow state name after the tick — Agent persists this against the session. */
44
46
  state: string;
45
47
  /** Whether the machine settled at a parked state. */
46
48
  parked: boolean;
47
- /** True when a state-timeout outcome matched no transition this tick — the host must NOT re-arm. */
49
+ /** True when a state-timeout outcome matched no transition this tick — the agent must NOT re-arm. */
48
50
  stateTimeoutUnhandled: boolean;
49
51
  /**
50
52
  * Accumulated LLM call count and token usage for this tick.
@@ -64,10 +66,10 @@ export interface TickOutput {
64
66
  };
65
67
  }
66
68
  /**
67
- * Pure agent: workflow definition + cognition machinery. No I/O, no transport,
68
- * no services. The Agent's only side channel is the `eff` handler it receives
69
- * on each `tick`. Server-side state (sessions, lanes, queue, outbox) lives in
70
- * `AgentHost` — not here.
69
+ * Workflow brain: workflow definition + cognition machinery. No I/O, no transport,
70
+ * no services. The brain's only side channel is the `eff` handler it receives
71
+ * on each `tick`. Body state (sessions, lanes, queue, outbox) lives in
72
+ * `Agent` — not here.
71
73
  *
72
74
  * The contract:
73
75
  *
@@ -75,13 +77,14 @@ export interface TickOutput {
75
77
  * `{ ctx, state, parked }`
76
78
  *
77
79
  * Reads top-to-bottom like a synchronous function. Every `await eff(...)`
78
- * inside the cognition core is a yield to the Server, which performs the
79
- * impure operation and resumes the Agent with the result.
80
+ * inside the cognition core is a yield to the body, which performs the
81
+ * impure operation and resumes the ZiggsBrain with the result.
80
82
  */
81
- export declare class Agent {
83
+ export declare class ZiggsBrain implements Brain {
84
+ readonly kind: "ziggs";
82
85
  workflow: Workflow;
83
86
  toolManager: ToolManager;
84
87
  promptBuilder: PromptBuilder;
85
- constructor({ workflow, toolManager, promptBuilder }: AgentOptions);
88
+ constructor({ workflow, toolManager, promptBuilder }: ZiggsBrainOptions);
86
89
  tick({ event, ctx, state, identity, eff, memory, transitionTap }: TickInput): Promise<TickOutput>;
87
90
  }
@@ -3,10 +3,10 @@ import { createDeliveryWriteKeys } from '../runtime/deliveryWriteKeys.js';
3
3
  import { runTurn } from '../runtime/runTurn.js';
4
4
  import { PromptBuilder } from '../runtime/PromptBuilder.js';
5
5
  /**
6
- * Pure agent: workflow definition + cognition machinery. No I/O, no transport,
7
- * no services. The Agent's only side channel is the `eff` handler it receives
8
- * on each `tick`. Server-side state (sessions, lanes, queue, outbox) lives in
9
- * `AgentHost` — not here.
6
+ * Workflow brain: workflow definition + cognition machinery. No I/O, no transport,
7
+ * no services. The brain's only side channel is the `eff` handler it receives
8
+ * on each `tick`. Body state (sessions, lanes, queue, outbox) lives in
9
+ * `Agent` — not here.
10
10
  *
11
11
  * The contract:
12
12
  *
@@ -14,27 +14,28 @@ import { PromptBuilder } from '../runtime/PromptBuilder.js';
14
14
  * `{ ctx, state, parked }`
15
15
  *
16
16
  * Reads top-to-bottom like a synchronous function. Every `await eff(...)`
17
- * inside the cognition core is a yield to the Server, which performs the
18
- * impure operation and resumes the Agent with the result.
17
+ * inside the cognition core is a yield to the body, which performs the
18
+ * impure operation and resumes the ZiggsBrain with the result.
19
19
  */
20
- export class Agent {
20
+ export class ZiggsBrain {
21
+ kind = 'ziggs';
21
22
  workflow;
22
23
  toolManager;
23
24
  promptBuilder;
24
25
  constructor({ workflow, toolManager, promptBuilder }) {
25
26
  if (!workflow?.states)
26
- throw new Error('Agent: workflow with states is required');
27
+ throw new Error('ZiggsBrain: workflow with states is required');
27
28
  if (!toolManager)
28
- throw new Error('Agent: toolManager is required');
29
+ throw new Error('ZiggsBrain: toolManager is required');
29
30
  this.workflow = workflow;
30
31
  this.toolManager = toolManager;
31
32
  this.promptBuilder = promptBuilder || new PromptBuilder();
32
33
  }
33
34
  async tick({ event, ctx, state, identity, eff, memory, transitionTap }) {
34
35
  if (!identity?.sessionId)
35
- throw new Error('Agent.tick: identity.sessionId is required');
36
+ throw new Error('ZiggsBrain.tick: identity.sessionId is required');
36
37
  if (!eff)
37
- throw new Error('Agent.tick: eff is required');
38
+ throw new Error('ZiggsBrain.tick: eff is required');
38
39
  const machine = new AgentMachine({
39
40
  workflow: this.workflow,
40
41
  executionCore: runTurn,
@@ -2,7 +2,7 @@
2
2
  * Shared Claude-Agent-SDK cognition pieces.
3
3
  *
4
4
  * Everything a Claude-brain agent needs to turn a Ziggs wake into a `query()`
5
- * call, EXCEPT the loop itself. `ClaudeSdkAgent` owns the loop (drive `query()`,
5
+ * call, EXCEPT the loop itself. `ClaudeBrain` owns the loop (drive `query()`,
6
6
  * collect metrics); this module owns the periphery so it lives in one place and
7
7
  * is unit-testable without spinning up the SDK.
8
8
  *
@@ -2,7 +2,7 @@
2
2
  * Shared Claude-Agent-SDK cognition pieces.
3
3
  *
4
4
  * Everything a Claude-brain agent needs to turn a Ziggs wake into a `query()`
5
- * call, EXCEPT the loop itself. `ClaudeSdkAgent` owns the loop (drive `query()`,
5
+ * call, EXCEPT the loop itself. `ClaudeBrain` owns the loop (drive `query()`,
6
6
  * collect metrics); this module owns the periphery so it lives in one place and
7
7
  * is unit-testable without spinning up the SDK.
8
8
  *
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Operating contract for Claude Agent SDK agents on Ziggs (AgentHost path).
2
+ * Operating contract for Claude Agent SDK agents on Ziggs (Agent path).
3
3
  *
4
4
  * The host reads the agent's inbox for it — a per-agent long-poll loop that
5
5
  * pulls each event concerning the agent, hands it to the brain, and owns the
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Operating contract for Claude Agent SDK agents on Ziggs (AgentHost path).
2
+ * Operating contract for Claude Agent SDK agents on Ziggs (Agent path).
3
3
  *
4
4
  * The host reads the agent's inbox for it — a per-agent long-poll loop that
5
5
  * pulls each event concerning the agent, hands it to the brain, and owns the