@ziggs-ai/agent-sdk 0.9.0 → 0.10.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.
Files changed (55) hide show
  1. package/CHANGELOG.md +23 -0
  2. package/dist/AgentHost.d.ts +53 -14
  3. package/dist/AgentHost.js +137 -8
  4. package/dist/agent/ClaudeSdkAgent.d.ts +27 -2
  5. package/dist/agent/ClaudeSdkAgent.js +151 -78
  6. package/dist/agent/claudeSdkTools.js +39 -11
  7. package/dist/context/applyEffects.js +4 -3
  8. package/dist/defineAgent.js +4 -1
  9. package/dist/formatters/HistoryFormatter.js +11 -0
  10. package/dist/index.d.ts +3 -2
  11. package/dist/index.js +3 -2
  12. package/dist/ingress/normalizeIncoming.d.ts +5 -0
  13. package/dist/ingress/normalizeIncoming.js +23 -1
  14. package/dist/pricing/fleetDefaults.d.ts +0 -3
  15. package/dist/pricing/fleetDefaults.js +0 -6
  16. package/dist/runtime/PromptBuilder.js +9 -5
  17. package/dist/runtime/bootstrapCtx.js +6 -8
  18. package/dist/runtime/buildOutcome.d.ts +9 -1
  19. package/dist/runtime/buildOutcome.js +29 -6
  20. package/dist/runtime/runTurn.js +1 -1
  21. package/dist/runtime/taskBookkeeping.d.ts +0 -2
  22. package/dist/runtime/taskBookkeeping.js +0 -6
  23. package/dist/runtime/taskIntake.js +6 -4
  24. package/dist/runtime/wakeWriteKeys.js +23 -21
  25. package/dist/server/ConnectionPool.d.ts +10 -52
  26. package/dist/server/ConnectionPool.js +26 -57
  27. package/dist/server/EventQueue.d.ts +6 -0
  28. package/dist/server/EventQueue.js +21 -6
  29. package/dist/server/SeenMessages.d.ts +7 -0
  30. package/dist/server/SeenMessages.js +15 -0
  31. package/dist/server/ZiggsEffectHandler.d.ts +1 -0
  32. package/dist/server/ZiggsEffectHandler.js +21 -11
  33. package/dist/server/inboxLoop.d.ts +17 -2
  34. package/dist/server/inboxLoop.js +92 -16
  35. package/dist/server/polls/PollKind.d.ts +72 -0
  36. package/dist/server/polls/PollKind.js +53 -0
  37. package/dist/server/polls/PollRunner.d.ts +62 -0
  38. package/dist/server/polls/PollRunner.js +138 -0
  39. package/dist/server/questMatch.d.ts +5 -0
  40. package/dist/server/questMatch.js +14 -0
  41. package/dist/server/runLauncher.d.ts +0 -6
  42. package/dist/server/runLauncher.js +54 -27
  43. package/dist/server/tasks/TaskService.d.ts +2 -11
  44. package/dist/server/tools/tier1/protocolRunner.d.ts +3 -1
  45. package/dist/server/tools/tier1/protocolRunner.js +40 -41
  46. package/dist/server/tools/tier1/protocolTools.js +23 -13
  47. package/dist/server/tools/tier2/connectionTools.js +12 -1
  48. package/dist/tasks/protocolRegistry.d.ts +27 -0
  49. package/dist/tasks/protocolRegistry.js +36 -0
  50. package/dist/tools/capabilityAdapter.js +12 -1
  51. package/dist/tools/recordReport.js +8 -4
  52. package/dist/types.d.ts +24 -11
  53. package/package.json +2 -2
  54. package/dist/server/staleBacklogGate.d.ts +0 -57
  55. package/dist/server/staleBacklogGate.js +0 -100
@@ -1,5 +1,5 @@
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
5
  import { createWakeWriteKeys } from '../runtime/wakeWriteKeys.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)
@@ -155,7 +175,7 @@ function resolveOnBehalfAgreementId(rawEvent, contextSnap, agentId, sessionId) {
155
175
  export class ClaudeSdkAgent {
156
176
  workflow;
157
177
  toolManager;
158
- description;
178
+ rolePrompt;
159
179
  specialization;
160
180
  model;
161
181
  anthropicKey;
@@ -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');
@@ -173,7 +200,10 @@ export class ClaudeSdkAgent {
173
200
  throw new Error('ClaudeSdkAgent: anthropicKey is required');
174
201
  this.workflow = opts.workflow;
175
202
  this.toolManager = opts.toolManager;
176
- this.description = opts.description;
203
+ // ZIG-1080: the prompt and the store blurb are two different strings. Agents
204
+ // written before the split still carry the prompt in `description`, so fall
205
+ // back to it rather than silently dropping their role.
206
+ this.rolePrompt = opts.systemPrompt || opts.description;
177
207
  this.specialization = opts.specialization ?? null;
178
208
  this.model = opts.model;
179
209
  this.anthropicKey = opts.anthropicKey;
@@ -206,7 +236,7 @@ export class ClaudeSdkAgent {
206
236
  };
207
237
  }
208
238
  nextCtx.recentWakes = [...recentWakes, now];
209
- if (isNoticeOnlyWake(rawEvent)) {
239
+ if (isNoticeOnlyWake(rawEvent, identity.agentId)) {
210
240
  runtimeLog.info('ClaudeSdkAgent', `notice-only wake — skipping LLM session=${identity.sessionId} agent=${this.agentId} ` +
211
241
  `type=${rawEvent?.type ?? 'unknown'}`);
212
242
  return {
@@ -219,9 +249,13 @@ export class ClaudeSdkAgent {
219
249
  }
220
250
  let contextSnap = null;
221
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.
222
255
  contextSnap = await eff({
223
256
  kind: 'read-context',
224
257
  sessionId: identity.sessionId,
258
+ opts: { maxMessages: 12 },
225
259
  });
226
260
  }
227
261
  catch (err) {
@@ -259,7 +293,7 @@ export class ClaudeSdkAgent {
259
293
  }
260
294
  const roleBlock = [
261
295
  `## Your role`,
262
- this.description,
296
+ this.rolePrompt,
263
297
  this.specialization ? `Specialization: ${this.specialization}` : '',
264
298
  ].filter(Boolean).join('\n');
265
299
  const systemPrompt = `${CLAUDE_SDK_PROTOCOL}\n\n${roleBlock}`;
@@ -284,13 +318,22 @@ export class ClaudeSdkAgent {
284
318
  runId,
285
319
  wakeKeys,
286
320
  };
287
- const availableTools = await this.toolManager.getAvailableTools();
288
- const mcpServer = buildZiggsSdkMcpServer(availableTools, () => toolContext, {
289
- name: 'ziggs',
290
- instructions: 'In-process Ziggs tools backed by api-client. No external MCP process.',
291
- });
321
+ this.toolContextHolder.current = toolContext;
322
+ if (!this.sdkMcpServer) {
323
+ const availableTools = await this.toolManager.getAvailableTools();
324
+ this.sdkMcpServer = buildZiggsSdkMcpServer(availableTools, () => {
325
+ const ctx = this.toolContextHolder.current;
326
+ if (!ctx) {
327
+ throw new Error('ClaudeSdkAgent: tool context missing for in-process MCP call');
328
+ }
329
+ return ctx;
330
+ }, {
331
+ name: 'ziggs',
332
+ instructions: 'In-process Ziggs tools backed by api-client. No external MCP process.',
333
+ });
334
+ }
292
335
  const prompt = buildClaudeEventPrompt(rawEvent, summarizeContext(contextSnap));
293
- const mcpServers = { ziggs: mcpServer };
336
+ const mcpServers = { ziggs: this.sdkMcpServer };
294
337
  assertHostedAgentZiggsTools(mcpServers);
295
338
  const queryOptions = claudeQueryOptions({
296
339
  systemPrompt,
@@ -303,80 +346,110 @@ export class ClaudeSdkAgent {
303
346
  if (line)
304
347
  runtimeLog.warn('ClaudeSdkAgent', `stderr session=${identity.sessionId}: ${line}`);
305
348
  },
306
- // ZIG-860: stateless-per-wake never resumes — each wake is a fresh
307
- // transcript, so per-wake input stays flat instead of replaying the
308
- // whole lane history every turn.
309
- resume: this.statelessPerWake ? undefined : nextCtx.claudeSessionId,
310
349
  });
311
350
  let metrics = { llmCalls: 0, tokens: { total: 0, input: 0, output: 0, cacheRead: 0, cacheCreation: 0 } };
312
351
  let sessionId = nextCtx.claudeSessionId;
313
352
  let spentInput = 0;
314
- // Host watchdog abort (see TickInput.signal): stop consuming AND interrupt
315
- // the query, so a timed-out wake never lingers as a token-burning zombie
316
- // racing its own redelivered retry. Breaking the for-await returns the
317
- // generator; interrupt() additionally stops a query blocked mid-turn.
318
- const query = this.queryFn({ prompt, options: queryOptions });
319
- const onAbort = () => {
320
- void query.interrupt?.()?.catch(() => { });
321
- };
322
- if (signal?.aborted)
323
- onAbort();
324
- else
325
- signal?.addEventListener('abort', onAbort, { once: true });
326
- try {
327
- for await (const message of query) {
328
- if (signal?.aborted) {
329
- runtimeLog.warn('ClaudeSdkAgent', `host watchdog aborted wake session=${identity.sessionId} agent=${this.agentId} — stopping query`);
330
- break;
331
- }
332
- if ('session_id' in message && typeof message.session_id === 'string') {
333
- sessionId = message.session_id;
334
- }
335
- // Surface the model's own prose so a hired Claude agent is as legible as
336
- // an FSM agent (ZIG-787). Each completed assistant turn's text is recorded
337
- // as a `thought` — the SAME agent-private channel the FSM uses for its
338
- // reasoning (runTurn `emitThought` → record-event → recordThought), so it
339
- // shows in the hired-agent view without cluttering the counterparty chat.
340
- // Pure tool-use turns carry no text and record nothing.
341
- //
342
- // Fire-and-forget on purpose: this runs inside `for await (query())`, so
343
- // awaiting an artifacts round-trip per turn would couple model-stream
344
- // consumption to the artifacts backend — a slow/wedged backend would stall
345
- // the whole wake, not just fail a breadcrumb. Recording is best-effort and
346
- // thought ordering is not load-bearing, so we let it run in the background
347
- // and swallow failures with a warn.
348
- if (message.type === 'assistant') {
349
- const text = extractAssistantText(message).trim();
350
- if (text) {
351
- // Ordinal drawn synchronously so the fire-and-forget below can't
352
- // reorder key assignment against other writes of this wake.
353
- const idempotencyKey = wakeKeys.artifactKey();
354
- void eff({ kind: 'record-event', sessionId: identity.sessionId, entry: { kind: 'thought', text }, idempotencyKey })
355
- .catch((err) => runtimeLog.warn('ClaudeSdkAgent', `record thought failed session=${identity.sessionId}: ${err.message}`));
353
+ // ZIG-860: stateless-per-wake never resumes — each wake is a fresh
354
+ // transcript, so per-wake input stays flat instead of replaying the whole
355
+ // lane history every turn.
356
+ const resumeId = this.statelessPerWake ? undefined : nextCtx.claudeSessionId;
357
+ const consumeQuery = async (resume) => {
358
+ // Host watchdog abort (see TickInput.signal): stop consuming AND interrupt
359
+ // the query, so a timed-out wake never lingers as a token-burning zombie
360
+ // racing its own redelivered retry. Breaking the for-await returns the
361
+ // generator; interrupt() additionally stops a query blocked mid-turn.
362
+ const query = this.queryFn({ prompt, options: { ...queryOptions, resume } });
363
+ const onAbort = () => {
364
+ void query.interrupt?.()?.catch(() => { });
365
+ };
366
+ if (signal?.aborted)
367
+ onAbort();
368
+ else
369
+ signal?.addEventListener('abort', onAbort, { once: true });
370
+ try {
371
+ for await (const message of query) {
372
+ if (signal?.aborted) {
373
+ runtimeLog.warn('ClaudeSdkAgent', `host watchdog aborted wake session=${identity.sessionId} agent=${this.agentId} — stopping query`);
374
+ break;
375
+ }
376
+ if ('session_id' in message && typeof message.session_id === 'string') {
377
+ sessionId = message.session_id;
378
+ }
379
+ // Surface the model's own prose so a hired Claude agent is as legible as
380
+ // an FSM agent (ZIG-787). Each completed assistant turn's text is recorded
381
+ // as a `thought` — the SAME agent-private channel the FSM uses for its
382
+ // reasoning (runTurn `emitThought` → record-event → recordThought), so it
383
+ // shows in the hired-agent view without cluttering the counterparty chat.
384
+ // Pure tool-use turns carry no text and record nothing.
385
+ //
386
+ // Fire-and-forget on purpose: this runs inside `for await (query())`, so
387
+ // awaiting an artifacts round-trip per turn would couple model-stream
388
+ // consumption to the artifacts backend — a slow/wedged backend would stall
389
+ // the whole wake, not just fail a breadcrumb. Recording is best-effort and
390
+ // thought ordering is not load-bearing, so we let it run in the background
391
+ // and swallow failures with a warn.
392
+ if (message.type === 'assistant') {
393
+ const text = extractAssistantText(message).trim();
394
+ if (text) {
395
+ // Ordinal drawn synchronously so the fire-and-forget below can't
396
+ // reorder key assignment against other writes of this wake.
397
+ const idempotencyKey = wakeKeys.artifactKey({ kind: 'thought', text });
398
+ void eff({ kind: 'record-event', sessionId: identity.sessionId, entry: { kind: 'thought', text }, idempotencyKey })
399
+ .catch((err) => runtimeLog.warn('ClaudeSdkAgent', `record thought failed session=${identity.sessionId}: ${err.message}`));
400
+ }
401
+ }
402
+ const m = extractMetrics(message);
403
+ if (m)
404
+ metrics = m;
405
+ if (message.type === 'result' && message.subtype !== 'success') {
406
+ runtimeLog.warn('ClaudeSdkAgent', `query ended without success subtype=${message.subtype} session=${identity.sessionId}`);
407
+ }
408
+ // Runaway guard (ZIG-759): abort before an unbounded loop drains the
409
+ // shared fleet key. Breaking the for-await calls the generator's return(),
410
+ // which terminates the underlying query.
411
+ spentInput += billableInput(usageOf(message));
412
+ if (spentInput > MAX_TASK_INPUT_TOKENS) {
413
+ 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)`);
414
+ break;
356
415
  }
357
416
  }
358
- const m = extractMetrics(message);
359
- if (m)
360
- metrics = m;
361
- if (message.type === 'result' && message.subtype !== 'success') {
362
- runtimeLog.warn('ClaudeSdkAgent', `query ended without success subtype=${message.subtype} session=${identity.sessionId}`);
363
- }
364
- // Runaway guard (ZIG-759): abort before an unbounded loop drains the
365
- // shared fleet key. Breaking the for-await calls the generator's return(),
366
- // which terminates the underlying query.
367
- spentInput += billableInput(usageOf(message));
368
- if (spentInput > MAX_TASK_INPUT_TOKENS) {
369
- 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)`);
370
- break;
371
- }
372
417
  }
418
+ finally {
419
+ signal?.removeEventListener('abort', onAbort);
420
+ }
421
+ };
422
+ try {
423
+ await consumeQuery(resumeId);
373
424
  }
374
425
  catch (err) {
375
- runtimeLog.warn('ClaudeSdkAgent', `query failed session=${identity.sessionId}: ${err.message}`);
376
- throw err;
377
- }
378
- finally {
379
- signal?.removeEventListener('abort', onAbort);
426
+ // ZIG-1191: the resumed transcript is gone. Claude Code stores transcripts
427
+ // on the container's own disk while the session id outlives it — persisted
428
+ // per lane in the session store with a 24h TTL — so a redeployed agents
429
+ // task inherits an id pointing at a file that no longer exists. Every wake
430
+ // on that lane then failed identically for as long as the TTL held, and the
431
+ // human saw one "Something went wrong. Please try again." per attempt.
432
+ //
433
+ // Retrying here is safe precisely because this failure happens BEFORE any
434
+ // turn runs: nothing was emitted, no tool ran, no idempotency ordinal was
435
+ // spent. That is why the retry is scoped to this one error and to one
436
+ // attempt — a mid-wake failure must keep propagating, since its partial
437
+ // writes are already out.
438
+ if (!isMissingSessionError(err) || !resumeId) {
439
+ runtimeLog.warn('ClaudeSdkAgent', `query failed session=${identity.sessionId}: ${err.message}`);
440
+ throw err;
441
+ }
442
+ runtimeLog.info('ClaudeSdkAgent', `resumed Claude session ${resumeId} no longer exists (container replaced?) ` +
443
+ `session=${identity.sessionId} agent=${this.agentId} — retrying once on a fresh session (ZIG-1191)`);
444
+ nextCtx.claudeSessionId = undefined;
445
+ sessionId = undefined;
446
+ try {
447
+ await consumeQuery(undefined);
448
+ }
449
+ catch (retryErr) {
450
+ runtimeLog.warn('ClaudeSdkAgent', `query failed session=${identity.sessionId}: ${retryErr.message}`);
451
+ throw retryErr;
452
+ }
380
453
  }
381
454
  // Stateless-per-wake keeps no session id — clear any carried over from the
382
455
  // prior ctx so the next wake starts fresh too (ZIG-860). Otherwise remember
@@ -8,18 +8,37 @@ import { listGrantsTool } from '../server/tools/tier2/grantsTools.js';
8
8
  // messages cover "what woke me and what was just said"; the model can pass a
9
9
  // larger maxMessages explicitly when it genuinely needs deeper history.
10
10
  const DEFAULT_SNAPSHOT_MAX_MESSAGES = 12;
11
+ /**
12
+ * First usable string among the candidates, treating blank as absent.
13
+ *
14
+ * ZIG-1081: a model that does not know a value fills the field with `""` rather
15
+ * than omitting it — the schema shows `chatId`, the agent has no way to know it,
16
+ * so it sends an empty string. Plain `??` let that blank win over the lane
17
+ * default, and the very next line rejected it as missing. Every wake opened with
18
+ * a failed `context_snapshot` and a recovery turn.
19
+ */
20
+ function firstFilled(...candidates) {
21
+ for (const candidate of candidates) {
22
+ if (typeof candidate === 'string' && candidate.trim() !== '')
23
+ return candidate;
24
+ }
25
+ return undefined;
26
+ }
11
27
  // Named distinctly from ziggs-mcp's per-type `ziggs_context_read`: this is the
12
28
  // one-shot aggregate snapshot (grant-fenced server-side), not a cursored read.
13
29
  export const contextSnapshotTool = defineTool('context_snapshot', {
14
- chatId: { type: 'string', description: 'Chat/session id. Defaults to the current lane session.' },
30
+ chatId: { type: 'string', description: 'Omit this to snapshot the chat you were woken in — that is the usual case, and you do not need to know its id. Only pass a chatId to snapshot a DIFFERENT chat.' },
15
31
  maxMessages: { type: 'number', description: `Message history cap (default ${DEFAULT_SNAPSHOT_MAX_MESSAGES}). Pass a larger value when you need deeper history.` },
16
32
  }, async (args, ctx) => {
17
33
  const c = ctx;
18
34
  if (!c.eff)
19
35
  throw new Error('context_snapshot requires eff in tool context');
20
- const sessionId = args.chatId ?? c.chatId ?? c.sessionId;
21
- if (!sessionId)
22
- throw new Error('context_snapshot: chatId is required');
36
+ const sessionId = firstFilled(args.chatId, c.chatId, c.sessionId);
37
+ if (!sessionId) {
38
+ throw new Error('context_snapshot: this wake carries no chat to snapshot. Call it with no arguments ' +
39
+ 'to read the chat you were woken in; if this wake is on an agreement lane with no ' +
40
+ 'single chat, read the agreement instead and pass a chatId from one of its chats.');
41
+ }
23
42
  return c.eff({
24
43
  kind: 'read-context',
25
44
  sessionId,
@@ -29,22 +48,24 @@ export const contextSnapshotTool = defineTool('context_snapshot', {
29
48
  });
30
49
  }, { description: 'Read the chat snapshot: the NEWEST maxMessages of history (chronological order), plus agreements, agents, and users for a session.' });
31
50
  export const chatSendTool = defineTool('chat_send', {
32
- chatId: { type: 'string', description: 'Chat id. Defaults to current session.' },
33
- receiverId: { type: 'string', description: 'Recipient user or agent id. Defaults to the sender of the event that woke you — omit when replying to them.' },
51
+ chatId: { type: 'string', description: 'Omit this to send in the chat you were woken in — that is the usual case. Only pass a chatId to send in a DIFFERENT chat.' },
52
+ receiverId: { type: 'string', description: 'Recipient user or agent id. Omit when replying to whoever woke you — that is the default.' },
34
53
  text: { type: 'string', required: true },
35
54
  idempotencyKey: { type: 'string', description: 'Reuse for retry-safe resend of the same logical message.' },
36
55
  }, async (args, ctx) => {
37
56
  const c = ctx;
38
57
  if (!c.eff)
39
58
  throw new Error('chat_send requires eff in tool context');
40
- const sessionId = args.chatId ?? c.chatId ?? c.sessionId;
41
- if (!sessionId)
42
- throw new Error('chat_send: chatId is required');
59
+ const sessionId = firstFilled(args.chatId, c.chatId, c.sessionId);
60
+ if (!sessionId) {
61
+ throw new Error('chat_send: this wake carries no chat to send in. Call it with no chatId to reply in ' +
62
+ 'the chat you were woken in, or pass the chatId of the chat you mean.');
63
+ }
43
64
  // Replying to whoever woke you is the overwhelmingly common case; making
44
65
  // the model spell it out cost a failed tool call + a full extra LLM turn
45
66
  // per reply. Batch/timer wakes carry no sender — those still require an
46
67
  // explicit receiverId.
47
- const receiverId = args.receiverId ?? c.senderId;
68
+ const receiverId = firstFilled(args.receiverId, c.senderId);
48
69
  if (!receiverId)
49
70
  throw new Error('chat_send: receiverId is required (this wake has no sender to default to)');
50
71
  // Default to the wake's content-derived key (ZIG-1045): a crash-replay that
@@ -99,12 +120,19 @@ export const listTasksTool = defineTool('task_list', {
99
120
  if (!c.agentId)
100
121
  throw new Error('task_list: agentId missing from tool context');
101
122
  const assignedTo = args.assignedToMe ? c.agentId : args.assignedTo;
123
+ // ZIG-1092: the lane fences this to the engagement's tasks. Without it the
124
+ // list is every task the agent's key can reach, across all its customers.
125
+ const laneId = c.chatId || c.sessionId || undefined;
102
126
  return listTasks({
103
127
  state: args.state,
104
128
  cursor: args.cursor,
105
129
  limit: typeof args.limit === 'number' ? args.limit : undefined,
106
130
  assignedTo,
107
- }, { operatorKey: c.operatorKey, agentId: c.agentId });
131
+ }, {
132
+ operatorKey: c.operatorKey,
133
+ agentId: c.agentId,
134
+ ...(laneId ? { laneId } : {}),
135
+ });
108
136
  }, {
109
137
  description: 'List tasks reachable by this agent (GET /tasks). Scope follows the operator key — same reach as chats and agreements. Optional state / assignee filters and cursor pagination. Use assignedToMe to list your own open work.',
110
138
  });
@@ -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')
@@ -35,7 +35,10 @@ export function defineAgent(input) {
35
35
  // states/transitions are validated too.
36
36
  const workflow = injectTaskIntakeDefault(injectKickoffDefault({
37
37
  id: input.agentId,
38
- description: input.description,
38
+ // The workflow description is the agent's role text, which PromptBuilder
39
+ // renders into <self>. ZIG-1080 split that from the store-facing
40
+ // `description`: prefer the prompt when the author supplied one.
41
+ description: input.systemPrompt || input.description,
39
42
  initial: input.initial,
40
43
  states: input.states,
41
44
  }));
@@ -126,6 +126,17 @@ export class HistoryFormatter {
126
126
  return 'System';
127
127
  if (id === agentId)
128
128
  return 'You';
129
+ // ZIG-1137: prefer presentation face when the wire masks the principal.
130
+ const presentation = participant['presentation'];
131
+ const faceName = (typeof presentation?.['name'] === 'string' && presentation['name']) ||
132
+ (typeof presentation?.['persona']?.['name'] === 'string'
133
+ ? presentation['persona']['name']
134
+ : null);
135
+ if (faceName)
136
+ return `${faceName} (${id})`;
137
+ if (id.startsWith('rpb_') || id.startsWith('psn_')) {
138
+ return `${id} (presentation)`;
139
+ }
129
140
  return `${id} (${participant['type'] || 'unknown'})`;
130
141
  }
131
142
  formatOwner(task, agentId) {
package/dist/index.d.ts CHANGED
@@ -41,7 +41,6 @@ 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,7 @@ 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, } from '@ziggs-ai/api-client';
69
70
  export { SeenMessages } from './server/SeenMessages.js';
70
71
  export { runtimeLog, resetRuntimeLogLevelCache } from './shared/runtimeLog.js';
71
72
  export type { RuntimeLogLevel } from './shared/runtimeLog.js';
package/dist/index.js CHANGED
@@ -35,7 +35,6 @@ export { default } from './AgentHost.js';
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,6 @@ 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, } from '@ziggs-ai/api-client';
74
75
  export { SeenMessages } from './server/SeenMessages.js';
75
76
  export { runtimeLog, resetRuntimeLogLevelCache } from './shared/runtimeLog.js';
@@ -71,6 +71,11 @@ interface WireMetadata {
71
71
  status?: string;
72
72
  };
73
73
  parties?: WireParties;
74
+ approvals?: Array<{
75
+ partyId?: string;
76
+ status?: string;
77
+ role?: string;
78
+ }>;
74
79
  };
75
80
  agreementId?: string;
76
81
  };
@@ -45,6 +45,27 @@ function isAlreadyActivatedProposalCreated(metadata) {
45
45
  const proposalStatus = agr.proposal?.status;
46
46
  return status === 'active' || proposalStatus === 'approved';
47
47
  }
48
+ /**
49
+ * ZIG-1078 — a `proposal_created` notification addressed to a party whose
50
+ * approval is PENDING is a consent REQUEST, not a status notice. The backend
51
+ * fans `proposal_created` out specifically to the pending approvers
52
+ * (ZIG-244), and parking it meant the hand-off provider (ZIG-1059) — or any
53
+ * agent whose consent a proposal awaits — was alerted and then never looked:
54
+ * the approval sat pending until a human nudged the chat. Same technique as
55
+ * isAlreadyActivatedProposalCreated (ZIG-1052): reclassify by what the
56
+ * embedded agreement SAYS rather than adding a new wake word.
57
+ */
58
+ function isPendingApprovalRequestNotification(metadata) {
59
+ if (wireOperation(metadata) !== 'proposal_created')
60
+ return false;
61
+ const receiverId = metadata.receiver?.id;
62
+ if (!receiverId)
63
+ return false;
64
+ const approvals = metadata.service?.agreement?.approvals;
65
+ if (!Array.isArray(approvals))
66
+ return false;
67
+ return approvals.some((a) => a && a.partyId === receiverId && a.status === 'pending');
68
+ }
48
69
  function isAgreementLifecycleNotification(metadata) {
49
70
  const entryType = metadata.entryType || 'message';
50
71
  if (entryType !== 'notification')
@@ -53,7 +74,8 @@ function isAgreementLifecycleNotification(metadata) {
53
74
  if (typeof operation === 'string' && PROPOSAL_RESOLVED_OPERATIONS.has(operation)) {
54
75
  return true;
55
76
  }
56
- return isAlreadyActivatedProposalCreated(metadata);
77
+ return (isAlreadyActivatedProposalCreated(metadata) ||
78
+ isPendingApprovalRequestNotification(metadata));
57
79
  }
58
80
  function isTerminalTaskState(state) {
59
81
  return state === 'completed' || state === 'failed' || state === 'cancelled';
@@ -19,7 +19,6 @@ export interface ProtocolPricingPayload {
19
19
  providerId?: string;
20
20
  executorId?: string;
21
21
  parentAgreementId?: string;
22
- parentTaskId?: string;
23
22
  [key: string]: unknown;
24
23
  }
25
24
  /**
@@ -36,8 +35,6 @@ export declare function applyFleetAgreementPricing(operation: 'agreement-propose
36
35
  * out unwidened by `ProtocolPricingPayload`'s `[key: string]: unknown` index.
37
36
  */
38
37
  export declare function applyFleetCounterPricing<T extends ProtocolPricingPayload>(payload: T, actingAgentId: string): T;
39
- /** User or agent direct-create / marketplace: default from provider specialist. */
40
- export declare function resolveDefaultPriceForProvider(providerId: string, description?: string | null, explicitPrice?: number | null): number | null;
41
38
  /**
42
39
  * Infer fleet actor when tool context omits agentId (ZIG-368 safety net).
43
40
  */
@@ -121,12 +121,6 @@ export function applyFleetCounterPricing(payload, actingAgentId) {
121
121
  return payload;
122
122
  return { ...payload, price: cents };
123
123
  }
124
- /** User or agent direct-create / marketplace: default from provider specialist. */
125
- export function resolveDefaultPriceForProvider(providerId, description, explicitPrice) {
126
- if (typeof explicitPrice === 'number')
127
- return explicitPrice;
128
- return resolveDefaultPriceCentsForExecutor(providerId, description);
129
- }
130
124
  /**
131
125
  * Infer fleet actor when tool context omits agentId (ZIG-368 safety net).
132
126
  */