@ziggs-ai/agent-sdk 0.10.0 → 0.10.2

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 (58) hide show
  1. package/CHANGELOG.md +23 -0
  2. package/README.md +9 -1
  3. package/dist/AgentHost.d.ts +50 -14
  4. package/dist/AgentHost.js +129 -7
  5. package/dist/agent/Agent.js +2 -2
  6. package/dist/agent/ClaudeSdkAgent.d.ts +23 -1
  7. package/dist/agent/ClaudeSdkAgent.js +156 -84
  8. package/dist/agent/claudeCognition.d.ts +25 -3
  9. package/dist/agent/claudeCognition.js +33 -1
  10. package/dist/agent/claudeSdkTools.d.ts +2 -2
  11. package/dist/agent/claudeSdkTools.js +1 -1
  12. package/dist/agent/toolManagerToSdkMcp.js +1 -1
  13. package/dist/context/applyEffects.js +4 -3
  14. package/dist/index.d.ts +6 -4
  15. package/dist/index.js +7 -3
  16. package/dist/ingress/normalizeIncoming.d.ts +2 -2
  17. package/dist/ingress/normalizeIncoming.js +1 -1
  18. package/dist/runtime/AgentMachine.d.ts +4 -4
  19. package/dist/runtime/AgentMachine.js +6 -6
  20. package/dist/runtime/bootstrapCtx.js +6 -8
  21. package/dist/runtime/buildOutcome.d.ts +9 -1
  22. package/dist/runtime/buildOutcome.js +29 -6
  23. package/dist/runtime/{wakeWriteKeys.d.ts → deliveryWriteKeys.d.ts} +2 -2
  24. package/dist/runtime/{wakeWriteKeys.js → deliveryWriteKeys.js} +15 -22
  25. package/dist/runtime/runMechanical.d.ts +2 -2
  26. package/dist/runtime/runMechanical.js +2 -2
  27. package/dist/runtime/runTurn.d.ts +2 -2
  28. package/dist/runtime/runTurn.js +5 -5
  29. package/dist/runtime/taskIntake.js +6 -4
  30. package/dist/server/ConnectionPool.d.ts +11 -53
  31. package/dist/server/ConnectionPool.js +29 -60
  32. package/dist/server/EventQueue.d.ts +6 -0
  33. package/dist/server/EventQueue.js +21 -6
  34. package/dist/server/SeenMessages.d.ts +7 -0
  35. package/dist/server/SeenMessages.js +15 -0
  36. package/dist/server/ZiggsEffectHandler.d.ts +1 -0
  37. package/dist/server/ZiggsEffectHandler.js +18 -11
  38. package/dist/server/createHealthServer.js +1 -1
  39. package/dist/server/inboxLoop.d.ts +17 -2
  40. package/dist/server/inboxLoop.js +92 -16
  41. package/dist/server/polls/PollKind.d.ts +72 -0
  42. package/dist/server/polls/PollKind.js +53 -0
  43. package/dist/server/polls/PollRunner.d.ts +62 -0
  44. package/dist/server/polls/PollRunner.js +138 -0
  45. package/dist/server/questMatch.d.ts +5 -0
  46. package/dist/server/questMatch.js +14 -0
  47. package/dist/server/runLauncher.d.ts +0 -6
  48. package/dist/server/runLauncher.js +54 -27
  49. package/dist/server/tools/tier1/protocolRunner.d.ts +4 -2
  50. package/dist/server/tools/tier1/protocolRunner.js +30 -34
  51. package/dist/server/tools/tier1/protocolTools.js +23 -13
  52. package/dist/server/tools/tier2/connectionTools.js +12 -1
  53. package/dist/tasks/protocolRegistry.d.ts +27 -0
  54. package/dist/tasks/protocolRegistry.js +36 -0
  55. package/dist/types.d.ts +4 -13
  56. package/package.json +2 -2
  57. package/dist/server/staleBacklogGate.d.ts +0 -57
  58. package/dist/server/staleBacklogGate.js +0 -100
@@ -1,8 +1,8 @@
1
1
  import { query } from '@anthropic-ai/claude-agent-sdk';
2
- import { outcomeFromEvent } from '../runtime/buildOutcome.js';
2
+ import { isMessageAddressedElsewhere, outcomeFromEvent } from '../runtime/buildOutcome.js';
3
3
  import { getBatchEvents } from '../context/batch.js';
4
4
  import { bootstrapCtx } from '../runtime/bootstrapCtx.js';
5
- import { createWakeWriteKeys } from '../runtime/wakeWriteKeys.js';
5
+ import { createDeliveryWriteKeys } from '../runtime/deliveryWriteKeys.js';
6
6
  import { CLAUDE_SDK_PROTOCOL } from './claudeSdkProtocol.js';
7
7
  import { buildZiggsSdkMcpServer } from './toolManagerToSdkMcp.js';
8
8
  import { claudeQueryOptions, buildClaudeEventPrompt, summarizeContext, extractAssistantText, } from './claudeCognition.js';
@@ -20,12 +20,18 @@ import { runtimeLog } from '../shared/runtimeLog.js';
20
20
  * `notification` only.
21
21
  *
22
22
  * Empty / missing batch → notice-only (do not burn a turn on a malformed wake).
23
+ *
24
+ * A message addressed to a DIFFERENT party is notice-only too (attention
25
+ * gate): the sender named who should act, and it wasn't this agent. The
26
+ * message still lands in chat history and arrives as context on the next
27
+ * real wake — skipped here is the LLM turn, not the information.
23
28
  */
24
- export function isNoticeOnlyWake(event) {
29
+ export function isNoticeOnlyWake(event, ownAgentId) {
25
30
  const events = getBatchEvents(event);
26
31
  if (events.length === 0)
27
32
  return true;
28
- return events.every((ev) => ev?.type === 'notification');
33
+ return events.every((ev) => ev?.type === 'notification' ||
34
+ isMessageAddressedElsewhere(ev, ownAgentId));
29
35
  }
30
36
  const DEFAULT_STATE = 'ready';
31
37
  /**
@@ -62,6 +68,20 @@ const MAX_WAKES_PER_WINDOW = Number(process.env.CLAUDE_SDK_MAX_WAKES_PER_WINDOW
62
68
  * discard. Env-overridable. Kept below MAX_TASK_INPUT_TOKENS so a lane resets
63
69
  * before it can approach the per-wake abort. */
64
70
  const MAX_SESSION_RESUME_INPUT_TOKENS = Number(process.env.CLAUDE_SDK_MAX_SESSION_INPUT_TOKENS ?? 200_000);
71
+ /**
72
+ * ZIG-1191 — "the transcript you asked me to resume is not here".
73
+ *
74
+ * Claude Code answers a `resume` of an unknown id with this, and it is the only
75
+ * failure that a fresh session reliably fixes: the id is valid-looking but its
76
+ * transcript lived on a container that is gone. Matched on the message because
77
+ * that is all the CLI gives us — no code, no typed error — so keep it narrow
78
+ * enough that a different failure never claims the retry.
79
+ */
80
+ export function isMissingSessionError(err) {
81
+ const message = err?.message;
82
+ return (typeof message === 'string' &&
83
+ /no conversation found with session id/i.test(message));
84
+ }
65
85
  /** Total billable input for one API response (base + cache read + cache write). */
66
86
  function billableInput(usage) {
67
87
  if (!usage)
@@ -164,6 +184,13 @@ export class ClaudeSdkAgent {
164
184
  maxTurns;
165
185
  statelessPerWake;
166
186
  queryFn;
187
+ /**
188
+ * ZIG-1120 #7 — tool set is fixed at host construction; rebuilding the
189
+ * in-process MCP server (+ zod schema tree) every wake was pure waste.
190
+ * A mutable holder supplies per-wake context without closing over `tick`.
191
+ */
192
+ toolContextHolder = { current: null };
193
+ sdkMcpServer = null;
167
194
  constructor(opts) {
168
195
  if (!opts.workflow?.states)
169
196
  throw new Error('ClaudeSdkAgent: workflow with states is required');
@@ -195,11 +222,11 @@ export class ClaudeSdkAgent {
195
222
  const nextCtx = bootstrapCtx(identity, ctx);
196
223
  nextCtx.lastOutcome = outcomeFromEvent(rawEvent ?? null, identity.agentId);
197
224
  const now = Date.now();
198
- const recentWakes = (nextCtx.recentWakes ?? []).filter((t) => now - t < WAKE_WINDOW_MS);
199
- if (recentWakes.length >= MAX_WAKES_PER_WINDOW) {
200
- runtimeLog.warn('ClaudeSdkAgent', `wake-rate breaker tripped: ${recentWakes.length} wakes in ${WAKE_WINDOW_MS / 60_000}min ` +
225
+ const recentDeliveries = (nextCtx.recentDeliveries ?? []).filter((t) => now - t < WAKE_WINDOW_MS);
226
+ if (recentDeliveries.length >= MAX_WAKES_PER_WINDOW) {
227
+ runtimeLog.warn('ClaudeSdkAgent', `wake-rate breaker tripped: ${recentDeliveries.length} wakes in ${WAKE_WINDOW_MS / 60_000}min ` +
201
228
  `session=${identity.sessionId} agent=${this.agentId} — skipping LLM for this wake`);
202
- nextCtx.recentWakes = recentWakes;
229
+ nextCtx.recentDeliveries = recentDeliveries;
203
230
  return {
204
231
  ctx: nextCtx,
205
232
  state: state ?? DEFAULT_STATE,
@@ -208,8 +235,8 @@ export class ClaudeSdkAgent {
208
235
  metrics: { llmCalls: 0, tokens: { total: 0, input: 0, output: 0 } },
209
236
  };
210
237
  }
211
- nextCtx.recentWakes = [...recentWakes, now];
212
- if (isNoticeOnlyWake(rawEvent)) {
238
+ nextCtx.recentDeliveries = [...recentDeliveries, now];
239
+ if (isNoticeOnlyWake(rawEvent, identity.agentId)) {
213
240
  runtimeLog.info('ClaudeSdkAgent', `notice-only wake — skipping LLM session=${identity.sessionId} agent=${this.agentId} ` +
214
241
  `type=${rawEvent?.type ?? 'unknown'}`);
215
242
  return {
@@ -222,9 +249,13 @@ export class ClaudeSdkAgent {
222
249
  }
223
250
  let contextSnap = null;
224
251
  try {
252
+ // ZIG-1120 #5: pre-read is for wake gating + summarizeContext (a count) +
253
+ // onBehalf agreement resolution — same 12-message cap as the tool path.
254
+ // Uncapped pulls burned 35–47k input tokens/wake on long chats.
225
255
  contextSnap = await eff({
226
256
  kind: 'read-context',
227
257
  sessionId: identity.sessionId,
258
+ opts: { maxMessages: 12 },
228
259
  });
229
260
  }
230
261
  catch (err) {
@@ -265,12 +296,14 @@ export class ClaudeSdkAgent {
265
296
  this.rolePrompt,
266
297
  this.specialization ? `Specialization: ${this.specialization}` : '',
267
298
  ].filter(Boolean).join('\n');
268
- const systemPrompt = `${CLAUDE_SDK_PROTOCOL}\n\n${roleBlock}`;
299
+ // 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 (ZIG-788).
301
+ const systemPrompt = [CLAUDE_SDK_PROTOCOL, roleBlock];
269
302
  const runId = crypto.randomUUID();
270
303
  // Replay-stable write keys for this wake (runId is random and can't serve):
271
304
  // a crash-replay of the same event derives the same keys, so the backend
272
305
  // drops the duplicate artifacts / no-ops the repeated task transitions.
273
- const wakeKeys = createWakeWriteKeys(identity.laneKey, rawEvent);
306
+ const writeKeys = createDeliveryWriteKeys(identity.laneKey, rawEvent);
274
307
  const toolContext = {
275
308
  operatorKey: this.operatorKey,
276
309
  agentId: this.agentId,
@@ -285,15 +318,24 @@ export class ClaudeSdkAgent {
285
318
  // not just task_result events — see resolveOnBehalfAgreementId.
286
319
  onBehalfOfAgreementId: resolveOnBehalfAgreementId(rawEvent, contextSnap, this.agentId, identity.sessionId),
287
320
  runId,
288
- wakeKeys,
321
+ writeKeys,
289
322
  };
290
- const availableTools = await this.toolManager.getAvailableTools();
291
- const mcpServer = buildZiggsSdkMcpServer(availableTools, () => toolContext, {
292
- name: 'ziggs',
293
- instructions: 'In-process Ziggs tools backed by api-client. No external MCP process.',
294
- });
323
+ this.toolContextHolder.current = toolContext;
324
+ if (!this.sdkMcpServer) {
325
+ const availableTools = await this.toolManager.getAvailableTools();
326
+ this.sdkMcpServer = buildZiggsSdkMcpServer(availableTools, () => {
327
+ const ctx = this.toolContextHolder.current;
328
+ if (!ctx) {
329
+ throw new Error('ClaudeSdkAgent: tool context missing for in-process MCP call');
330
+ }
331
+ return ctx;
332
+ }, {
333
+ name: 'ziggs',
334
+ instructions: 'In-process Ziggs tools backed by api-client. No external MCP process.',
335
+ });
336
+ }
295
337
  const prompt = buildClaudeEventPrompt(rawEvent, summarizeContext(contextSnap));
296
- const mcpServers = { ziggs: mcpServer };
338
+ const mcpServers = { ziggs: this.sdkMcpServer };
297
339
  assertHostedAgentZiggsTools(mcpServers);
298
340
  const queryOptions = claudeQueryOptions({
299
341
  systemPrompt,
@@ -306,80 +348,110 @@ export class ClaudeSdkAgent {
306
348
  if (line)
307
349
  runtimeLog.warn('ClaudeSdkAgent', `stderr session=${identity.sessionId}: ${line}`);
308
350
  },
309
- // ZIG-860: stateless-per-wake never resumes — each wake is a fresh
310
- // transcript, so per-wake input stays flat instead of replaying the
311
- // whole lane history every turn.
312
- resume: this.statelessPerWake ? undefined : nextCtx.claudeSessionId,
313
351
  });
314
352
  let metrics = { llmCalls: 0, tokens: { total: 0, input: 0, output: 0, cacheRead: 0, cacheCreation: 0 } };
315
353
  let sessionId = nextCtx.claudeSessionId;
316
354
  let spentInput = 0;
317
- // Host watchdog abort (see TickInput.signal): stop consuming AND interrupt
318
- // the query, so a timed-out wake never lingers as a token-burning zombie
319
- // racing its own redelivered retry. Breaking the for-await returns the
320
- // generator; interrupt() additionally stops a query blocked mid-turn.
321
- const query = this.queryFn({ prompt, options: queryOptions });
322
- const onAbort = () => {
323
- void query.interrupt?.()?.catch(() => { });
324
- };
325
- if (signal?.aborted)
326
- onAbort();
327
- else
328
- signal?.addEventListener('abort', onAbort, { once: true });
329
- try {
330
- for await (const message of query) {
331
- if (signal?.aborted) {
332
- runtimeLog.warn('ClaudeSdkAgent', `host watchdog aborted wake session=${identity.sessionId} agent=${this.agentId} — stopping query`);
333
- break;
334
- }
335
- if ('session_id' in message && typeof message.session_id === 'string') {
336
- sessionId = message.session_id;
337
- }
338
- // Surface the model's own prose so a hired Claude agent is as legible as
339
- // an FSM agent (ZIG-787). Each completed assistant turn's text is recorded
340
- // as a `thought` — the SAME agent-private channel the FSM uses for its
341
- // reasoning (runTurn `emitThought` → record-event → recordThought), so it
342
- // shows in the hired-agent view without cluttering the counterparty chat.
343
- // Pure tool-use turns carry no text and record nothing.
344
- //
345
- // Fire-and-forget on purpose: this runs inside `for await (query())`, so
346
- // awaiting an artifacts round-trip per turn would couple model-stream
347
- // consumption to the artifacts backend — a slow/wedged backend would stall
348
- // the whole wake, not just fail a breadcrumb. Recording is best-effort and
349
- // thought ordering is not load-bearing, so we let it run in the background
350
- // and swallow failures with a warn.
351
- if (message.type === 'assistant') {
352
- const text = extractAssistantText(message).trim();
353
- if (text) {
354
- // Ordinal drawn synchronously so the fire-and-forget below can't
355
- // reorder key assignment against other writes of this wake.
356
- const idempotencyKey = wakeKeys.artifactKey({ kind: 'thought', text });
357
- void eff({ kind: 'record-event', sessionId: identity.sessionId, entry: { kind: 'thought', text }, idempotencyKey })
358
- .catch((err) => runtimeLog.warn('ClaudeSdkAgent', `record thought failed session=${identity.sessionId}: ${err.message}`));
355
+ // ZIG-860: stateless-per-wake never resumes — each wake is a fresh
356
+ // transcript, so per-wake input stays flat instead of replaying the whole
357
+ // lane history every turn.
358
+ const resumeId = this.statelessPerWake ? undefined : nextCtx.claudeSessionId;
359
+ const consumeQuery = async (resume) => {
360
+ // Host watchdog abort (see TickInput.signal): stop consuming AND interrupt
361
+ // the query, so a timed-out wake never lingers as a token-burning zombie
362
+ // racing its own redelivered retry. Breaking the for-await returns the
363
+ // generator; interrupt() additionally stops a query blocked mid-turn.
364
+ const query = this.queryFn({ prompt, options: { ...queryOptions, resume } });
365
+ const onAbort = () => {
366
+ void query.interrupt?.()?.catch(() => { });
367
+ };
368
+ if (signal?.aborted)
369
+ onAbort();
370
+ else
371
+ signal?.addEventListener('abort', onAbort, { once: true });
372
+ try {
373
+ for await (const message of query) {
374
+ if (signal?.aborted) {
375
+ runtimeLog.warn('ClaudeSdkAgent', `host watchdog aborted wake session=${identity.sessionId} agent=${this.agentId} — stopping query`);
376
+ break;
377
+ }
378
+ if ('session_id' in message && typeof message.session_id === 'string') {
379
+ sessionId = message.session_id;
380
+ }
381
+ // Surface the model's own prose so a hired Claude agent is as legible as
382
+ // an FSM agent (ZIG-787). Each completed assistant turn's text is recorded
383
+ // as a `thought` — the SAME agent-private channel the FSM uses for its
384
+ // reasoning (runTurn `emitThought` → record-event → recordThought), so it
385
+ // shows in the hired-agent view without cluttering the counterparty chat.
386
+ // Pure tool-use turns carry no text and record nothing.
387
+ //
388
+ // Fire-and-forget on purpose: this runs inside `for await (query())`, so
389
+ // awaiting an artifacts round-trip per turn would couple model-stream
390
+ // consumption to the artifacts backend — a slow/wedged backend would stall
391
+ // the whole wake, not just fail a breadcrumb. Recording is best-effort and
392
+ // thought ordering is not load-bearing, so we let it run in the background
393
+ // and swallow failures with a warn.
394
+ if (message.type === 'assistant') {
395
+ const text = extractAssistantText(message).trim();
396
+ if (text) {
397
+ // Ordinal drawn synchronously so the fire-and-forget below can't
398
+ // reorder key assignment against other writes of this wake.
399
+ const idempotencyKey = writeKeys.artifactKey({ kind: 'thought', text });
400
+ void eff({ kind: 'record-event', sessionId: identity.sessionId, entry: { kind: 'thought', text }, idempotencyKey })
401
+ .catch((err) => runtimeLog.warn('ClaudeSdkAgent', `record thought failed session=${identity.sessionId}: ${err.message}`));
402
+ }
403
+ }
404
+ const m = extractMetrics(message);
405
+ if (m)
406
+ metrics = m;
407
+ if (message.type === 'result' && message.subtype !== 'success') {
408
+ runtimeLog.warn('ClaudeSdkAgent', `query ended without success subtype=${message.subtype} session=${identity.sessionId}`);
409
+ }
410
+ // Runaway guard (ZIG-759): abort before an unbounded loop drains the
411
+ // shared fleet key. Breaking the for-await calls the generator's return(),
412
+ // which terminates the underlying query.
413
+ spentInput += billableInput(usageOf(message));
414
+ if (spentInput > MAX_TASK_INPUT_TOKENS) {
415
+ runtimeLog.warn('ClaudeSdkAgent', `input-token budget exceeded (~${spentInput} > ${MAX_TASK_INPUT_TOKENS}) session=${identity.sessionId} agent=${this.agentId} — aborting query to cap spend (ZIG-759)`);
416
+ break;
359
417
  }
360
418
  }
361
- const m = extractMetrics(message);
362
- if (m)
363
- metrics = m;
364
- if (message.type === 'result' && message.subtype !== 'success') {
365
- runtimeLog.warn('ClaudeSdkAgent', `query ended without success subtype=${message.subtype} session=${identity.sessionId}`);
366
- }
367
- // Runaway guard (ZIG-759): abort before an unbounded loop drains the
368
- // shared fleet key. Breaking the for-await calls the generator's return(),
369
- // which terminates the underlying query.
370
- spentInput += billableInput(usageOf(message));
371
- if (spentInput > MAX_TASK_INPUT_TOKENS) {
372
- runtimeLog.warn('ClaudeSdkAgent', `input-token budget exceeded (~${spentInput} > ${MAX_TASK_INPUT_TOKENS}) session=${identity.sessionId} agent=${this.agentId} — aborting query to cap spend (ZIG-759)`);
373
- break;
374
- }
375
419
  }
420
+ finally {
421
+ signal?.removeEventListener('abort', onAbort);
422
+ }
423
+ };
424
+ try {
425
+ await consumeQuery(resumeId);
376
426
  }
377
427
  catch (err) {
378
- runtimeLog.warn('ClaudeSdkAgent', `query failed session=${identity.sessionId}: ${err.message}`);
379
- throw err;
380
- }
381
- finally {
382
- signal?.removeEventListener('abort', onAbort);
428
+ // ZIG-1191: the resumed transcript is gone. Claude Code stores transcripts
429
+ // on the container's own disk while the session id outlives it — persisted
430
+ // per lane in the session store with a 24h TTL — so a redeployed agents
431
+ // task inherits an id pointing at a file that no longer exists. Every wake
432
+ // on that lane then failed identically for as long as the TTL held, and the
433
+ // human saw one "Something went wrong. Please try again." per attempt.
434
+ //
435
+ // Retrying here is safe precisely because this failure happens BEFORE any
436
+ // turn runs: nothing was emitted, no tool ran, no idempotency ordinal was
437
+ // spent. That is why the retry is scoped to this one error and to one
438
+ // attempt — a mid-wake failure must keep propagating, since its partial
439
+ // writes are already out.
440
+ if (!isMissingSessionError(err) || !resumeId) {
441
+ runtimeLog.warn('ClaudeSdkAgent', `query failed session=${identity.sessionId}: ${err.message}`);
442
+ throw err;
443
+ }
444
+ 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 (ZIG-1191)`);
446
+ nextCtx.claudeSessionId = undefined;
447
+ sessionId = undefined;
448
+ try {
449
+ await consumeQuery(undefined);
450
+ }
451
+ catch (retryErr) {
452
+ runtimeLog.warn('ClaudeSdkAgent', `query failed session=${identity.sessionId}: ${retryErr.message}`);
453
+ throw retryErr;
454
+ }
383
455
  }
384
456
  // Stateless-per-wake keeps no session id — clear any carried over from the
385
457
  // prior ctx so the next wake starts fresh too (ZIG-860). Otherwise remember
@@ -11,11 +11,15 @@
11
11
  * it MUST be applied on every Claude-brain wake. Centralizing it here means a
12
12
  * caller cannot silently drift from it.
13
13
  */
14
- import type { Options, SDKAssistantMessage } from '@anthropic-ai/claude-agent-sdk';
14
+ import { type Options, type SDKAssistantMessage } from '@anthropic-ai/claude-agent-sdk';
15
15
  import type { RawEvent } from '../runtime/buildOutcome.js';
16
16
  export interface ClaudeQueryOptionsParams {
17
- /** Protocol + role system prompt. */
18
- systemPrompt: string;
17
+ /**
18
+ * The system prompt in cache-ordered blocks: the shared protocol first, then
19
+ * this agent's role. Both are stable across wakes — see
20
+ * {@link claudeQueryOptions} for why that matters.
21
+ */
22
+ systemPrompt: string[];
19
23
  /** Claude model id for the brain. */
20
24
  model: string;
21
25
  /** In-process MCP servers exposing the agent's tools (e.g. `{ ziggs }`). */
@@ -47,6 +51,24 @@ export declare function resolveClaudeModel(preferred?: string, servicesModel?: s
47
51
  * Build the `query()` options for a Claude-brain wake. The lockdown fields are
48
52
  * fixed here; callers only supply the per-wake variables (prompt, model, tools,
49
53
  * turn budget, resume). Returns `Options` so it drops straight into `query()`.
54
+ *
55
+ * ZIG-788 — prompt caching. The system prompt is handed over as blocks with a
56
+ * trailing `SYSTEM_PROMPT_DYNAMIC_BOUNDARY`: everything before the marker is
57
+ * eligible for cross-session caching, and there is nothing after it because
58
+ * nothing in this prompt varies per wake. Every per-wake input — the event, the
59
+ * context digest — is in the user prompt (`buildClaudeEventPrompt`), which sits
60
+ * after the cached prefix and cannot invalidate it.
61
+ *
62
+ * Cross-SESSION is the point, not cross-turn: `statelessPerWake` starts a fresh
63
+ * transcript on every wake, so without this the identical prefix was re-read at
64
+ * full price on each one.
65
+ *
66
+ * Caching is a prefix match, so block ORDER decides how far the hit reaches:
67
+ * the protocol block is byte-identical for every hosted agent and goes first,
68
+ * the role block is per-agent and goes second. An agent whose combined prompt
69
+ * lands under the model's minimum cacheable prefix (~1k tokens on Sonnet, more
70
+ * on Haiku) simply writes no entry — that is a silent no-op, not an error, so
71
+ * read `cacheRead`/`cacheCreation` in the wake metrics rather than assuming.
50
72
  */
51
73
  export declare function claudeQueryOptions(params: ClaudeQueryOptionsParams): Options;
52
74
  /**
@@ -1,3 +1,17 @@
1
+ /**
2
+ * Shared Claude-Agent-SDK cognition pieces.
3
+ *
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()`,
6
+ * collect metrics); this module owns the periphery so it lives in one place and
7
+ * is unit-testable without spinning up the SDK.
8
+ *
9
+ * The lockdown option set (`tools: []`, `strictMcpConfig`, `settingSources: []`,
10
+ * bypass permissions, no local settings) is a security boundary, not a default —
11
+ * it MUST be applied on every Claude-brain wake. Centralizing it here means a
12
+ * caller cannot silently drift from it.
13
+ */
14
+ import { SYSTEM_PROMPT_DYNAMIC_BOUNDARY, } from '@anthropic-ai/claude-agent-sdk';
1
15
  import { getBatchEvents } from '../context/batch.js';
2
16
  const DEFAULT_CLAUDE_MODEL = 'claude-sonnet-4-6';
3
17
  /**
@@ -17,10 +31,28 @@ export function resolveClaudeModel(preferred, servicesModel) {
17
31
  * Build the `query()` options for a Claude-brain wake. The lockdown fields are
18
32
  * fixed here; callers only supply the per-wake variables (prompt, model, tools,
19
33
  * turn budget, resume). Returns `Options` so it drops straight into `query()`.
34
+ *
35
+ * ZIG-788 — prompt caching. The system prompt is handed over as blocks with a
36
+ * trailing `SYSTEM_PROMPT_DYNAMIC_BOUNDARY`: everything before the marker is
37
+ * eligible for cross-session caching, and there is nothing after it because
38
+ * nothing in this prompt varies per wake. Every per-wake input — the event, the
39
+ * context digest — is in the user prompt (`buildClaudeEventPrompt`), which sits
40
+ * after the cached prefix and cannot invalidate it.
41
+ *
42
+ * Cross-SESSION is the point, not cross-turn: `statelessPerWake` starts a fresh
43
+ * transcript on every wake, so without this the identical prefix was re-read at
44
+ * full price on each one.
45
+ *
46
+ * Caching is a prefix match, so block ORDER decides how far the hit reaches:
47
+ * the protocol block is byte-identical for every hosted agent and goes first,
48
+ * the role block is per-agent and goes second. An agent whose combined prompt
49
+ * lands under the model's minimum cacheable prefix (~1k tokens on Sonnet, more
50
+ * on Haiku) simply writes no entry — that is a silent no-op, not an error, so
51
+ * read `cacheRead`/`cacheCreation` in the wake metrics rather than assuming.
20
52
  */
21
53
  export function claudeQueryOptions(params) {
22
54
  return {
23
- systemPrompt: params.systemPrompt,
55
+ systemPrompt: [...params.systemPrompt, SYSTEM_PROMPT_DYNAMIC_BOUNDARY],
24
56
  model: params.model,
25
57
  mcpServers: params.mcpServers,
26
58
  // The agent's ONLY capabilities are its MCP tools — no built-in file/bash/web
@@ -1,5 +1,5 @@
1
1
  import { type ToolDefinition } from '../tools/defineTool.js';
2
- import type { EffectHandler, WakeWriteKeys } from '../types.js';
2
+ import type { EffectHandler, DeliveryWriteKeys } from '../types.js';
3
3
  export interface ClaudeSdkToolContext extends Record<string, unknown> {
4
4
  operatorKey: string;
5
5
  agentId: string;
@@ -14,7 +14,7 @@ export interface ClaudeSdkToolContext extends Record<string, unknown> {
14
14
  /** Per-wake id grouping this tick's tool calls in telemetry (ZIG-775). Random — never use for idempotency. */
15
15
  runId?: string;
16
16
  /** Replay-stable write keys for this wake — write tools derive idempotency keys from here, never from the LLM. */
17
- wakeKeys?: WakeWriteKeys;
17
+ writeKeys?: DeliveryWriteKeys;
18
18
  }
19
19
  export declare const contextSnapshotTool: ToolDefinition;
20
20
  export declare const chatSendTool: ToolDefinition;
@@ -77,7 +77,7 @@ export const chatSendTool = defineTool('chat_send', {
77
77
  const text = String(args.text);
78
78
  const messageId = args.idempotencyKey
79
79
  ? `sdk_idem_${String(args.idempotencyKey)}`
80
- : c.wakeKeys?.messageKey({ text, sessionId, receiverId }) ?? `sdk_${randomUUID()}`;
80
+ : c.writeKeys?.messageKey({ text, sessionId, receiverId }) ?? `sdk_${randomUUID()}`;
81
81
  return c.eff({
82
82
  kind: 'send-message',
83
83
  sessionId,
@@ -100,7 +100,7 @@ export function buildSdkToolDefs(toolDefs, getContext) {
100
100
  senderType: ctx.senderType,
101
101
  onBehalfOfAgreementId: ctx.onBehalfOfAgreementId,
102
102
  runId: ctx.runId,
103
- wakeKeys: ctx.wakeKeys,
103
+ writeKeys: ctx.writeKeys,
104
104
  });
105
105
  if (r.ok)
106
106
  return toCallToolResult(r.result);
@@ -21,10 +21,11 @@ function applyTaskOperationResultToContext(updates, operation, result) {
21
21
  }
22
22
  }
23
23
  if (operation === 'task-update') {
24
- const status = (task?.['state'] || r['state'] || r['status']);
25
- if (status === 'completed')
24
+ // `state` only — ZIG-1057 cut `status` from the wire (ZIG-1128).
25
+ const state = (task?.['state'] || r['state']);
26
+ if (state === 'completed')
26
27
  updates['taskCompleted'] = true;
27
- if (status === 'failed')
28
+ if (state === 'failed')
28
29
  updates['taskFailed'] = true;
29
30
  }
30
31
  if (operation === 'agreement-respond')
package/dist/index.d.ts CHANGED
@@ -35,13 +35,12 @@ export { validateWorkflow, WorkflowValidationError } from './runtime/validateWor
35
35
  export { deriveRespondsToProposals, statesRespondToProposals, } from './runtime/deriveRespondsToProposals.js';
36
36
  export { outcomeFromEvent, outcomeFromActionResult, applyOutcomeToCtx } from './runtime/buildOutcome.js';
37
37
  export { LANE_BOOK_KEY, allCollectedResults, allDelegatedAgreementIds, allPendingSubtasks, } from './runtime/taskBookkeeping.js';
38
- export { createWakeWriteKeys } from './runtime/wakeWriteKeys.js';
39
- export type { Outcome, OutcomeKind, Ctx, Identity, HistoryEntry, ToolResultEntry, PendingSubtask, CollectedSubtaskResult, TaskBookkeeping, Workflow, State, ParkedState, ThinkingState, MechanicalState, Action, Transition, PromptDef, ActionPrompt, Produces, ProducesFn, AgreementRef, TaskRef, ProposalRef, AgentConfig, DefineAgentInput, ServicesConfig, Effect, EffectKind, EffectResult, EffectResultMap, EffectHandler, LlmMessage, LlmToolCall, LlmToolSchema, LlmResponse, ContextSnapshot, ReadContextOpts, ToolCallResult, RecordedEntry, WakeWriteKeys, } from './types.js';
38
+ export { createDeliveryWriteKeys } from './runtime/deliveryWriteKeys.js';
39
+ export type { Outcome, OutcomeKind, Ctx, Identity, HistoryEntry, ToolResultEntry, PendingSubtask, CollectedSubtaskResult, TaskBookkeeping, Workflow, State, ParkedState, ThinkingState, MechanicalState, Action, Transition, PromptDef, ActionPrompt, Produces, ProducesFn, AgreementRef, TaskRef, ProposalRef, AgentConfig, DefineAgentInput, ServicesConfig, Effect, EffectKind, EffectResult, EffectResultMap, EffectHandler, LlmMessage, LlmToolCall, LlmToolSchema, LlmResponse, ContextSnapshot, ReadContextOpts, ToolCallResult, RecordedEntry, DeliveryWriteKeys, } from './types.js';
40
40
  export { default } from './AgentHost.js';
41
41
  export { validateContext, ContextWarning } from './cognition/validateContext.js';
42
42
  export { runLauncher } from './server/runLauncher.js';
43
43
  export { ConnectionPool } from './server/ConnectionPool.js';
44
- export { createStaleBacklogGate, resolveReplayPolicy, newestActionableAt, parseReplayMaxAge, } from './server/staleBacklogGate.js';
45
44
  export { decodeOperatorKeyClaims, boundKeyFleetWarning } from './shared/operatorKeyClaims.js';
46
45
  export { createHealthServer } from './server/createHealthServer.js';
47
46
  export { TaskService } from './server/tasks/TaskService.js';
@@ -55,6 +54,8 @@ export { OpenAIAdapter } from './adapters/OpenAIAdapter.js';
55
54
  export { extractJSON, safeParseJSON } from './utils/jsonExtractor.js';
56
55
  export { HistoryFormatter, historyFormatter, AgreementFormatter, agreementFormatter } from './formatters/index.js';
57
56
  export { ProactiveTrigger } from './server/proactive/ProactiveTrigger.js';
57
+ export { PollRegistry, declaredPolls, type PollBinding, type PollContext, type PollKind, type PollResult, } from './server/polls/PollKind.js';
58
+ export { PollRunner, type PollRunnerOptions, type WakeTarget } from './server/polls/PollRunner.js';
58
59
  export { PROTOCOL_TOOLS, agreementProposeTool, agreementSubcontractTool, agreementRespondTool, agreementClaimTool, agreementCounterTool, agreementGetTool, agreementRevokeTool, agreementFulfillTool, taskCreateTool, taskSetResultTool, taskUpdatePlanStepTool, dispatchProtocolOp, PAYMENT_TOOLS, paymentBalanceTool, paymentTransferTool, paymentHoldTool, paymentReleaseTool, paymentResolveWalletTool, CONNECTION_TOOLS, connectionProxyTool, connectionListGrantsTool, requestConnectionTool, DISCOVERY_TOOLS, agentSearchTool, agentGetTool, MARKETPLACE_TOOLS, marketplaceViewTool, CONTEXT_TOOLS, contextReadTool, contextDelegateTool, contextExpandReachTool, contextDiscoverGrantableTool, GRANTS_TOOLS, listGrantsTool, LINK_TOOLS, linkCreateInviteTool, linkListTool, ARTIFACT_TOOLS, recordArtifactTool, CHAT_TOOLS, openConversationTool, } from './server/tools/index.js';
59
60
  export { CLAUDE_SDK_TOOLS } from './agent/claudeSdkTools.js';
60
61
  export { PROTOCOL_TOOL_NAMES, PROTOCOL_TOOL_TO_OPERATION, mapProtocolToolToOperation, isProtocolToolName } from './tasks/protocolRegistry.js';
@@ -65,7 +66,8 @@ export { classifyIncomingEvent } from './context/classifyEnvelope.js';
65
66
  export { buildContextUpdates, CONTEXT_RESET } from './context/applyEffects.js';
66
67
  export { getBatchEvents, isTaskResultRelevantToAgent } from './context/batch.js';
67
68
  export { classifyWorkflowEvent, findTaskResult, unwrapBatchEvent, findIncomingTaskResult } from './context/routingLabels.js';
68
- export { MessagesClient, ArtifactsClient, AgentSearchClient, getBackendUrl, getWebSocketUrl, } from '@ziggs-ai/api-client';
69
+ export { MessagesClient, ArtifactsClient, AgentSearchClient, getBackendUrl, configureApiClient, } from '@ziggs-ai/api-client';
70
+ export type { ApiClientConfig } from '@ziggs-ai/api-client';
69
71
  export { SeenMessages } from './server/SeenMessages.js';
70
72
  export { runtimeLog, resetRuntimeLogLevelCache } from './shared/runtimeLog.js';
71
73
  export type { RuntimeLogLevel } from './shared/runtimeLog.js';
package/dist/index.js CHANGED
@@ -29,13 +29,12 @@ export { validateWorkflow, WorkflowValidationError } from './runtime/validateWor
29
29
  export { deriveRespondsToProposals, statesRespondToProposals, } from './runtime/deriveRespondsToProposals.js';
30
30
  export { outcomeFromEvent, outcomeFromActionResult, applyOutcomeToCtx } from './runtime/buildOutcome.js';
31
31
  export { LANE_BOOK_KEY, allCollectedResults, allDelegatedAgreementIds, allPendingSubtasks, } from './runtime/taskBookkeeping.js';
32
- export { createWakeWriteKeys } from './runtime/wakeWriteKeys.js';
32
+ export { createDeliveryWriteKeys } from './runtime/deliveryWriteKeys.js';
33
33
  export { default } from './AgentHost.js';
34
34
  // ── Supporting modules ──────────────────────────────────────────────────────
35
35
  export { validateContext, ContextWarning } from './cognition/validateContext.js';
36
36
  export { runLauncher } from './server/runLauncher.js';
37
37
  export { ConnectionPool } from './server/ConnectionPool.js';
38
- export { createStaleBacklogGate, resolveReplayPolicy, newestActionableAt, parseReplayMaxAge, } from './server/staleBacklogGate.js';
39
38
  export { decodeOperatorKeyClaims, boundKeyFleetWarning } from './shared/operatorKeyClaims.js';
40
39
  export { createHealthServer } from './server/createHealthServer.js';
41
40
  export { TaskService } from './server/tasks/TaskService.js';
@@ -48,6 +47,8 @@ export { OpenAIAdapter } from './adapters/OpenAIAdapter.js';
48
47
  export { extractJSON, safeParseJSON } from './utils/jsonExtractor.js';
49
48
  export { HistoryFormatter, historyFormatter, AgreementFormatter, agreementFormatter } from './formatters/index.js';
50
49
  export { ProactiveTrigger } from './server/proactive/ProactiveTrigger.js';
50
+ export { PollRegistry, declaredPolls, } from './server/polls/PollKind.js';
51
+ export { PollRunner } from './server/polls/PollRunner.js';
51
52
  // ZIG-894 — the hand-rolled ZiggsPayClient / ZiggsConnectClient were deleted;
52
53
  // payment and connection tools now use @ziggs-ai/api-client's PaymentsClient /
53
54
  // ConnectionsClient, the same clients ziggs-mcp uses.
@@ -70,6 +71,9 @@ export { classifyIncomingEvent } from './context/classifyEnvelope.js';
70
71
  export { buildContextUpdates, CONTEXT_RESET } from './context/applyEffects.js';
71
72
  export { getBatchEvents, isTaskResultRelevantToAgent } from './context/batch.js';
72
73
  export { classifyWorkflowEvent, findTaskResult, unwrapBatchEvent, findIncomingTaskResult } from './context/routingLabels.js';
73
- export { MessagesClient, ArtifactsClient, AgentSearchClient, getBackendUrl, getWebSocketUrl, } from '@ziggs-ai/api-client';
74
+ export { MessagesClient, ArtifactsClient, AgentSearchClient, getBackendUrl,
75
+ // ZIG-1204 — self-hosted / non-default backend: consumers of agent-sdk alone
76
+ // must be able to point the shared HTTP client without a direct api-client dep.
77
+ configureApiClient, } from '@ziggs-ai/api-client';
74
78
  export { SeenMessages } from './server/SeenMessages.js';
75
79
  export { runtimeLog, resetRuntimeLogLevelCache } from './shared/runtimeLog.js';
@@ -7,7 +7,7 @@ export interface NormalizedEvent {
7
7
  /**
8
8
  * Backend per-delivery id of the chat row this event came from. The one
9
9
  * field of the event that is identical on live push and on catch-up
10
- * redelivery, so it anchors the wake's idempotency keys (wakeWriteKeys).
10
+ * redelivery, so it anchors the wake's idempotency keys (deliveryWriteKeys).
11
11
  */
12
12
  messageId?: string;
13
13
  timestamp: number;
@@ -92,7 +92,7 @@ interface NormalizeArgs {
92
92
  * what is fenced to this agent, so there is no client-side "is this for me"
93
93
  * relevance gate — this function decides SHAPE (message vs task_result vs
94
94
  * agreement_lifecycle vs notification) and the fields the FSM / SessionResolver /
95
- * wakeWriteKeys depend on, nothing more. `ownAgentId` is accepted for call-site
95
+ * deliveryWriteKeys depend on, nothing more. `ownAgentId` is accepted for call-site
96
96
  * compatibility but no longer gates anything.
97
97
  */
98
98
  export declare function normalizeIncomingEvent({ text, metadata }: NormalizeArgs): NormalizedEvent;
@@ -86,7 +86,7 @@ function isTerminalTaskState(state) {
86
86
  * what is fenced to this agent, so there is no client-side "is this for me"
87
87
  * relevance gate — this function decides SHAPE (message vs task_result vs
88
88
  * agreement_lifecycle vs notification) and the fields the FSM / SessionResolver /
89
- * wakeWriteKeys depend on, nothing more. `ownAgentId` is accepted for call-site
89
+ * deliveryWriteKeys depend on, nothing more. `ownAgentId` is accepted for call-site
90
90
  * compatibility but no longer gates anything.
91
91
  */
92
92
  export function normalizeIncomingEvent({ text, metadata = {} }) {
@@ -1,4 +1,4 @@
1
- import type { Ctx, Outcome, Workflow, Identity, EffectHandler, WakeWriteKeys } from '../types.js';
1
+ import type { Ctx, Outcome, Workflow, Identity, EffectHandler, DeliveryWriteKeys } from '../types.js';
2
2
  import { type RawEvent } from './buildOutcome.js';
3
3
  import type { runTurn as RunTurnFn } from './runTurn.js';
4
4
  import { PromptBuilder } from './PromptBuilder.js';
@@ -60,7 +60,7 @@ interface MachineOptions {
60
60
  /** Runaway-loop backstop: max thinking turns without human input before parking. Default 100. */
61
61
  maxTurnsWithoutUserInput?: number;
62
62
  /** This wake's replay-stable write keys — one stream shared by every turn of the tick. */
63
- wakeKeys?: WakeWriteKeys;
63
+ writeKeys?: DeliveryWriteKeys;
64
64
  }
65
65
  interface IncomingFrame {
66
66
  event?: RawEvent;
@@ -86,7 +86,7 @@ export declare class AgentMachine {
86
86
  toolManager: ToolManager;
87
87
  promptBuilder: PromptBuilder;
88
88
  memory?: MemoryStore;
89
- wakeKeys?: WakeWriteKeys;
89
+ writeKeys?: DeliveryWriteKeys;
90
90
  state: string;
91
91
  status: 'active' | 'stopped';
92
92
  ctx: Ctx;
@@ -114,7 +114,7 @@ export declare class AgentMachine {
114
114
  output: number;
115
115
  };
116
116
  get stateTimeoutUnhandled(): boolean;
117
- constructor({ workflow, executionCore, eff, toolManager, promptBuilder, memory, input, transitionTap, maxTurnsWithoutUserInput, wakeKeys }: MachineOptions);
117
+ constructor({ workflow, executionCore, eff, toolManager, promptBuilder, memory, input, transitionTap, maxTurnsWithoutUserInput, writeKeys }: MachineOptions);
118
118
  getSnapshot(): MachineSnapshot;
119
119
  /** Parked = currently in a parked state and not executing a turn. */
120
120
  get isParked(): boolean;