@ziggs-ai/agent-sdk 0.10.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 (41) hide show
  1. package/CHANGELOG.md +23 -0
  2. package/dist/AgentHost.d.ts +50 -14
  3. package/dist/AgentHost.js +129 -7
  4. package/dist/agent/ClaudeSdkAgent.d.ts +23 -1
  5. package/dist/agent/ClaudeSdkAgent.js +145 -75
  6. package/dist/context/applyEffects.js +4 -3
  7. package/dist/index.d.ts +3 -2
  8. package/dist/index.js +3 -2
  9. package/dist/runtime/bootstrapCtx.js +6 -8
  10. package/dist/runtime/buildOutcome.d.ts +9 -1
  11. package/dist/runtime/buildOutcome.js +29 -6
  12. package/dist/runtime/taskIntake.js +6 -4
  13. package/dist/runtime/wakeWriteKeys.js +14 -21
  14. package/dist/server/ConnectionPool.d.ts +10 -52
  15. package/dist/server/ConnectionPool.js +26 -57
  16. package/dist/server/EventQueue.d.ts +6 -0
  17. package/dist/server/EventQueue.js +21 -6
  18. package/dist/server/SeenMessages.d.ts +7 -0
  19. package/dist/server/SeenMessages.js +15 -0
  20. package/dist/server/ZiggsEffectHandler.d.ts +1 -0
  21. package/dist/server/ZiggsEffectHandler.js +14 -7
  22. package/dist/server/inboxLoop.d.ts +17 -2
  23. package/dist/server/inboxLoop.js +92 -16
  24. package/dist/server/polls/PollKind.d.ts +72 -0
  25. package/dist/server/polls/PollKind.js +53 -0
  26. package/dist/server/polls/PollRunner.d.ts +62 -0
  27. package/dist/server/polls/PollRunner.js +138 -0
  28. package/dist/server/questMatch.d.ts +5 -0
  29. package/dist/server/questMatch.js +14 -0
  30. package/dist/server/runLauncher.d.ts +0 -6
  31. package/dist/server/runLauncher.js +54 -27
  32. package/dist/server/tools/tier1/protocolRunner.d.ts +3 -1
  33. package/dist/server/tools/tier1/protocolRunner.js +24 -28
  34. package/dist/server/tools/tier1/protocolTools.js +23 -13
  35. package/dist/server/tools/tier2/connectionTools.js +12 -1
  36. package/dist/tasks/protocolRegistry.d.ts +27 -0
  37. package/dist/tasks/protocolRegistry.js +36 -0
  38. package/dist/types.d.ts +0 -9
  39. package/package.json +2 -2
  40. package/dist/server/staleBacklogGate.d.ts +0 -57
  41. package/dist/server/staleBacklogGate.js +0 -100
package/CHANGELOG.md CHANGED
@@ -1,5 +1,28 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.10.0 — unreleased
4
+
5
+ ### Removed (breaking)
6
+
7
+ - Removed exports `createStaleBacklogGate`, `resolveReplayPolicy`, `newestActionableAt`, `parseReplayMaxAge`. The launcher rewrite deleted the wake machinery these gated: every agent stays connected and the write signals it, so there is no backlog to age out and nothing to replay-gate (ZIG-993). Pinned to 0.4.x and importing any of the four? Delete the call — the behaviour it bought is now unconditional. There is no replacement API.
8
+ - `FLEET_REPLAY_MAX_AGE` is no longer read; the replay window it set no longer exists. Nothing in this repo sets it, but it can come out of any deployment env that does.
9
+ - `Ctx` no longer carries `pendingSubtasks`, `collectedResults`, or `delegatedAgreementIds`. ZIG-977 moved the real bookkeeping into `Ctx.tasks` (keyed by taskId) and ZIG-1057 stopped syncing these mirrors, but they stayed on the interface as required-and-always-empty — so a workflow guard reading one saw empty forever and silently checked nothing. Read via `allPendingSubtasks(ctx)` / `allCollectedResults(ctx)` / `allDelegatedAgreementIds(ctx)`. Sessions persisted before `tasks` existed are still migrated on restore.
10
+ - `WireResult.status` removed. ZIG-1057 cut `status` from the wire in favour of `state`; four readers kept a `|| r.status` tail, which would have absorbed a backend regression instead of failing on it. Task state resolves as `task?.state || r.state`.
11
+
12
+ ### Added
13
+
14
+ - Declared polls: an agent names the external sources it depends on (`polls`, `pollKinds`) and never executes them. New exports `PollRunner`, `PollRegistry`, `declaredPolls`, and types `PollBinding`, `PollContext`, `PollKind`, `PollResult`, `PollRunnerOptions`, `WakeTarget`. `runLauncher` runs declared polls at fleet level — outside any `AgentHost`, because a host is evicted when idle and that is exactly when its source still needs watching.
15
+
16
+ ### Changed
17
+
18
+ - Attention gate: only the addressed agent spends a turn. A message addressed elsewhere in a shared room no longer wakes every participant.
19
+ - No turn on a lane whose agreement has ended, and none on the echo of a task that already reported — both previously cost a turn and produced nothing.
20
+ - Persona wire shapes are understood on inbound frames.
21
+ - A wake declares its lane so the backend can fence reach (ZIG-1092).
22
+ - A proposal awaiting the agent's own consent counts as actionable rather than a notice to park (ZIG-1078).
23
+ - Artifact idempotency keys are derived from content (ZIG-1083).
24
+ - Blank tool arguments fall back to the wake's lane (ZIG-1081).
25
+
3
26
  ## 0.9.0 — 2026-07-30
4
27
 
5
28
  ### Changed (breaking)
@@ -76,6 +76,12 @@ export interface AgentHostOptions {
76
76
  /** Claude-brain system prompt (ZIG-1080); falls back to `description`. */
77
77
  systemPrompt?: string;
78
78
  specialization?: string | null;
79
+ /**
80
+ * ZIG-1185 — strings compared exactly against a quest's `match` field.
81
+ * Miss → ack with zero LLM. Match → normal wake. `specialization` is also
82
+ * checked when set.
83
+ */
84
+ questMatchTags?: string[];
79
85
  tools?: ToolDefinition[];
80
86
  taskTools?: 'all' | 'none' | string[];
81
87
  /**
@@ -120,14 +126,6 @@ export interface AgentHostOptions {
120
126
  sessionStore?: SessionStore;
121
127
  /** Observability tap: called after every effect with the effect and its response. */
122
128
  effectTap?: (effect: import('./types.js').Effect, response: unknown) => void;
123
- /**
124
- * Liveness signal: called whenever the host receives or processes work (an
125
- * inbox message or resource event delivered to a lane). Hosting pools use it
126
- * to reset their idle-eviction timer — without it a host that is actively
127
- * working (but never addressed via pool.sendTo) is torn down mid-stream at
128
- * the idle timeout, discarding lane state, Claude session ids, and timers.
129
- */
130
- onActivity?: () => void;
131
129
  /** Observability tap: called on every FSM state transition, no-match, depth-guard, or error-escape (ZIG-442). */
132
130
  transitionTap?: (event: import('./runtime/AgentMachine.js').TransitionEvent) => void;
133
131
  /**
@@ -198,11 +196,54 @@ export declare class AgentHost {
198
196
  handleResourceEvent(event: Record<string, unknown>): Promise<void>;
199
197
  /** Wire entry. Normalize → resolve session → enqueue on the right lane. */
200
198
  handleMessage(text: string, metadata?: Record<string, unknown>): Promise<void>;
199
+ /**
200
+ * ZIG-1144 part 3 — an agreement lane outlives the agreement.
201
+ *
202
+ * A lane named `agrn-<agreementId>` keeps its Claude session for the whole
203
+ * engagement, so a trailing event after the deal ends pays for all of it: on
204
+ * 2026-08-03 a cancelled hand-off woke its lane once for 92k tokens, and on
205
+ * 08-04 the same shape cost 147k with the session reaching 399,864 input tokens
206
+ * before it reset. Nothing was produced either time — the work had finished and
207
+ * the contract was gone.
208
+ *
209
+ * So a lane whose agreement is terminal does not get a turn. The event is still
210
+ * consumed (and acked by the inbox loop), the FSM is untouched, and a chat lane
211
+ * is never affected: only the agreement-scoped routing key is checked.
212
+ *
213
+ * Failing open is deliberate. If the lookup errors we run the turn — a missed
214
+ * suppression costs tokens, while a wrongly suppressed turn drops work.
215
+ */
216
+ private laneAgreementIsTerminal;
217
+ /**
218
+ * ZIG-1153 — the trailing turn on work that is already reported.
219
+ *
220
+ * A lane session carries the whole engagement, so the LAST wake is the most
221
+ * expensive one: on prod, the frame that followed a completed elicitation cost
222
+ * **158,733 tokens** with the session's input at 366,417 before ZIG-816 reset
223
+ * it, and produced nothing — the spec had been written and the task closed one
224
+ * frame earlier.
225
+ *
226
+ * A task frame that carries a terminal state AND a result is an echo of work
227
+ * this agent has already finished: there is no decision left in it. Dropping it
228
+ * is not the same suppression as {@link laneAgreementIsTerminal}, which waits
229
+ * for the agreement to end — this fires while the engagement is still live,
230
+ * which is where the cost actually was.
231
+ *
232
+ * Deliberately narrow: only a `task_result` frame, only with a terminal state,
233
+ * and only when a result is attached. A completed task with no result recorded
234
+ * still gets its turn, because that is an agent with something left to say.
235
+ */
236
+ private isSettledTaskEcho;
201
237
  private _processEvent;
202
238
  /** Arms the lane's liveness timer iff stateName is a parked state declaring timeoutMs. */
203
239
  private _armIfTimedParked;
204
240
  private _resolveOwnAgentId;
205
241
  connectAsync(_timeout?: number): Promise<this>;
242
+ /**
243
+ * ZIG-1185 — plain string compare against {@link AgentHostOptions.questMatchTags}
244
+ * and `specialization`. No match → return (caller acks). Match → normal wake.
245
+ */
246
+ private triageQuestDelivery;
206
247
  private _bootstrapAssignedActiveTasks;
207
248
  /** ZIG-802: replay task-assigned for an existing active task (demo recovery). */
208
249
  wakeTaskAssignment(taskId: string): Promise<void>;
@@ -217,10 +258,5 @@ export declare class AgentHost {
217
258
  * createAgentPool([cfg, ...]) → ConnectionPool (lazy fleet, async-loaded)
218
259
  */
219
260
  export declare function createAgent(options?: AgentHostOptions | AgentConfig): AgentHost;
220
- export declare function createAgentPool(configs: AgentHostOptions[] | AgentConfig[], opts?: {
221
- poolOptions?: {
222
- maxActive?: number;
223
- idleTimeoutMs?: number;
224
- };
225
- }): Promise<unknown>;
261
+ export declare function createAgentPool(configs: AgentHostOptions[] | AgentConfig[]): Promise<unknown>;
226
262
  export default AgentHost;
package/dist/AgentHost.js CHANGED
@@ -18,6 +18,7 @@ import { ArtifactsClient, ContextReadClient, TelemetryClient, sendChatMessage, }
18
18
  import { TaskService } from './server/tasks/TaskService.js';
19
19
  import { AgreementService } from './server/agreements/AgreementService.js';
20
20
  import { runInboxLoop } from './server/inboxLoop.js';
21
+ import { questExactMatch } from './server/questMatch.js';
21
22
  import { SeenMessages } from './server/SeenMessages.js';
22
23
  import { runtimeLog } from './shared/runtimeLog.js';
23
24
  /**
@@ -292,7 +293,14 @@ export class AgentHost {
292
293
  promptBuilder: services.promptBuilder,
293
294
  });
294
295
  this.eventQueue = new EventQueue((event, laneKey) => this._processEvent(event, laneKey));
295
- this.stateTimers = new StateTimers((ev, laneKey) => { void this.eventQueue.enqueue(ev, laneKey); });
296
+ // A timer frame is not a delivery: there is no watermark waiting on it and
297
+ // nothing to redeliver, so its failure is logged and dropped here rather
298
+ // than left as an unhandled rejection (ZIG-1191 made enqueue reject).
299
+ this.stateTimers = new StateTimers((ev, laneKey) => {
300
+ void this.eventQueue.enqueue(ev, laneKey).catch((err) => {
301
+ runtimeLog.warn('AgentHost', `state-timer frame failed laneKey=${laneKey}: ${err?.message || err}`);
302
+ });
303
+ });
296
304
  }
297
305
  /**
298
306
  * Handler for a resource change (artifact created, task state transition,
@@ -371,12 +379,96 @@ export class AgentHost {
371
379
  // The inbox loop owns ack (act-before-ack, delivery law); the host only
372
380
  // routes the event onto its lane. No live-path watermark ack — there is no
373
381
  // separate live/push channel to reconcile against anymore.
374
- return this.eventQueue.enqueue(normalized.event, laneKey);
382
+ //
383
+ // ZIG-1191: a failure un-marks the messageId. The mark above is what stops
384
+ // the same message being processed twice; leaving it set on a failed wake
385
+ // would make the redelivery this rejection buys us a no-op.
386
+ try {
387
+ await this.eventQueue.enqueue(normalized.event, laneKey);
388
+ }
389
+ catch (err) {
390
+ if (messageId)
391
+ this.seenMessages.unmark(messageId);
392
+ throw err;
393
+ }
394
+ }
395
+ /**
396
+ * ZIG-1144 part 3 — an agreement lane outlives the agreement.
397
+ *
398
+ * A lane named `agrn-<agreementId>` keeps its Claude session for the whole
399
+ * engagement, so a trailing event after the deal ends pays for all of it: on
400
+ * 2026-08-03 a cancelled hand-off woke its lane once for 92k tokens, and on
401
+ * 08-04 the same shape cost 147k with the session reaching 399,864 input tokens
402
+ * before it reset. Nothing was produced either time — the work had finished and
403
+ * the contract was gone.
404
+ *
405
+ * So a lane whose agreement is terminal does not get a turn. The event is still
406
+ * consumed (and acked by the inbox loop), the FSM is untouched, and a chat lane
407
+ * is never affected: only the agreement-scoped routing key is checked.
408
+ *
409
+ * Failing open is deliberate. If the lookup errors we run the turn — a missed
410
+ * suppression costs tokens, while a wrongly suppressed turn drops work.
411
+ */
412
+ async laneAgreementIsTerminal(sessionId) {
413
+ if (!sessionId.startsWith('agrn-'))
414
+ return false;
415
+ const agreementId = sessionId.slice('agrn-'.length);
416
+ if (!agreementId || !this.agreementService)
417
+ return false;
418
+ try {
419
+ const agreement = await this.agreementService.get(agreementId);
420
+ const status = String(agreement?.status ?? '');
421
+ return (status === 'cancelled' || status === 'refunded' || status === 'fulfilled');
422
+ }
423
+ catch {
424
+ return false;
425
+ }
426
+ }
427
+ /**
428
+ * ZIG-1153 — the trailing turn on work that is already reported.
429
+ *
430
+ * A lane session carries the whole engagement, so the LAST wake is the most
431
+ * expensive one: on prod, the frame that followed a completed elicitation cost
432
+ * **158,733 tokens** with the session's input at 366,417 before ZIG-816 reset
433
+ * it, and produced nothing — the spec had been written and the task closed one
434
+ * frame earlier.
435
+ *
436
+ * A task frame that carries a terminal state AND a result is an echo of work
437
+ * this agent has already finished: there is no decision left in it. Dropping it
438
+ * is not the same suppression as {@link laneAgreementIsTerminal}, which waits
439
+ * for the agreement to end — this fires while the engagement is still live,
440
+ * which is where the cost actually was.
441
+ *
442
+ * Deliberately narrow: only a `task_result` frame, only with a terminal state,
443
+ * and only when a result is attached. A completed task with no result recorded
444
+ * still gets its turn, because that is an agent with something left to say.
445
+ */
446
+ isSettledTaskEcho(event) {
447
+ const frame = event;
448
+ if (frame?.type !== 'task_result')
449
+ return false;
450
+ const result = frame.result;
451
+ if (!result)
452
+ return false;
453
+ const state = String(result.state ?? '');
454
+ if (state !== 'completed' && state !== 'cancelled' && state !== 'failed') {
455
+ return false;
456
+ }
457
+ const reported = result.result;
458
+ return reported !== undefined && reported !== null && reported !== '';
375
459
  }
376
460
  async _processEvent(event, laneKey) {
377
461
  const session = this.sessions.get(laneKey);
378
462
  if (!session?.sessionId)
379
463
  return;
464
+ if (await this.laneAgreementIsTerminal(session.sessionId)) {
465
+ runtimeLog.info(this.options.name || 'agent', `skipping wake on ${session.sessionId} — its agreement is terminal`);
466
+ return;
467
+ }
468
+ if (this.isSettledTaskEcho(event)) {
469
+ runtimeLog.info(this.options.name || 'agent', `skipping wake on ${session.sessionId} — its task already reported a result`);
470
+ return;
471
+ }
380
472
  // Stale timer frames (superseded epoch) are dropped BEFORE touching the
381
473
  // live timer — a pure-stale frame must not reset a freshly armed countdown.
382
474
  const frame = this.stateTimers.stripStale(event, laneKey);
@@ -479,6 +571,13 @@ export class AgentHost {
479
571
  if (senderId && senderType === 'USER') {
480
572
  this.eff({ kind: 'send-message', sessionId: session.sessionId, receiverId: senderId, text: 'Something went wrong. Please try again.' }).catch(() => { });
481
573
  }
574
+ // ZIG-1191: and then say so. Everything above is recovery for the lane —
575
+ // it does not mean the event was handled, and the caller is about to
576
+ // decide whether the delivery that carried it can be acked. Swallowing
577
+ // here is what turned a failed wake into a lost one: two hand-off publish
578
+ // notifications were acked after failing, so nothing ever redelivered
579
+ // them and a claim stayed blocked on consent that no wake would give.
580
+ throw err;
482
581
  }
483
582
  finally {
484
583
  clearTimeout(tickWatchdog);
@@ -517,18 +616,15 @@ export class AgentHost {
517
616
  seen: this.seenMessages,
518
617
  label,
519
618
  onMessage: (text, metadata) => {
520
- this.options.onActivity?.();
521
619
  return this.handleMessage(text, metadata);
522
620
  },
523
621
  onResourceEvent: (event) => {
524
- this.options.onActivity?.();
525
622
  return this.handleResourceEvent(event);
526
623
  },
527
624
  // ZIG-973: work delivered by the inbox's task channel, not by a chat
528
625
  // row — this is what reaches a task under a chat-less agreement or one
529
626
  // the agent assigned to itself.
530
627
  onTask: async (task) => {
531
- this.options.onActivity?.();
532
628
  try {
533
629
  await this.wakeTaskAssignment(task.taskId);
534
630
  }
@@ -536,10 +632,35 @@ export class AgentHost {
536
632
  runtimeLog.warn(this.options.name || 'agent', `inbox task ${task.taskId} could not be woken: ${err.message}`);
537
633
  }
538
634
  },
635
+ // ZIG-1185: exact-match triage in code — miss acks with 0 tokens.
636
+ onQuest: async (quest) => {
637
+ await this.triageQuestDelivery(quest);
638
+ },
539
639
  });
540
640
  }
541
641
  return this;
542
642
  }
643
+ /**
644
+ * ZIG-1185 — plain string compare against {@link AgentHostOptions.questMatchTags}
645
+ * and `specialization`. No match → return (caller acks). Match → normal wake.
646
+ */
647
+ async triageQuestDelivery(quest) {
648
+ const tags = [
649
+ ...(this.options.questMatchTags ?? []),
650
+ ...(this.options.specialization ? [this.options.specialization] : []),
651
+ ];
652
+ if (!questExactMatch(quest.match, tags)) {
653
+ runtimeLog.debug(this.options.name || 'agent', `quest ${quest.agreementId}: no tag match for ${JSON.stringify(quest.match)} — ack without wake`);
654
+ return;
655
+ }
656
+ const needle = quest.match.trim();
657
+ const title = quest.title?.trim() || needle;
658
+ await this.handleMessage(`New marketplace quest matching your tags (${needle}): ${title}. Claim with agreement_claim if you will take it.`, {
659
+ messageId: `quest-wake:${quest.agreementId}:${quest.ts}`,
660
+ chatId: `marketplace:${quest.agreementId}`,
661
+ agreementId: quest.agreementId,
662
+ });
663
+ }
543
664
  async _bootstrapAssignedActiveTasks() {
544
665
  const agentId = this.options.agentId;
545
666
  if (!agentId || !this.taskService)
@@ -626,10 +747,11 @@ export class AgentHost {
626
747
  export function createAgent(options = {}) {
627
748
  return new AgentHost(options);
628
749
  }
629
- export async function createAgentPool(configs, opts = {}) {
750
+ export async function createAgentPool(configs) {
630
751
  const ConnectionPool = await loadConnectionPool();
631
- const pool = new ConnectionPool(opts.poolOptions);
752
+ const pool = new ConnectionPool();
632
753
  pool.register(configs);
754
+ await pool.connectAll();
633
755
  return pool;
634
756
  }
635
757
  export default AgentHost;
@@ -15,8 +15,13 @@ import type { TickInput, TickOutput } from './Agent.js';
15
15
  * `notification` only.
16
16
  *
17
17
  * Empty / missing batch → notice-only (do not burn a turn on a malformed wake).
18
+ *
19
+ * A message addressed to a DIFFERENT party is notice-only too (attention
20
+ * gate): the sender named who should act, and it wasn't this agent. The
21
+ * message still lands in chat history and arrives as context on the next
22
+ * real wake — skipped here is the LLM turn, not the information.
18
23
  */
19
- export declare function isNoticeOnlyWake(event: RawEvent | undefined): boolean;
24
+ export declare function isNoticeOnlyWake(event: RawEvent | undefined, ownAgentId?: string | null): boolean;
20
25
  export type CognitionKind = 'fsm' | 'claude-sdk';
21
26
  export interface ClaudeSdkAgentOptions {
22
27
  workflow: Workflow;
@@ -38,6 +43,16 @@ export interface ClaudeSdkAgentOptions {
38
43
  /** Injectable for tests. Defaults to @anthropic-ai/claude-agent-sdk query(). */
39
44
  queryFn?: typeof query;
40
45
  }
46
+ /**
47
+ * ZIG-1191 — "the transcript you asked me to resume is not here".
48
+ *
49
+ * Claude Code answers a `resume` of an unknown id with this, and it is the only
50
+ * failure that a fresh session reliably fixes: the id is valid-looking but its
51
+ * transcript lived on a container that is gone. Matched on the message because
52
+ * that is all the CLI gives us — no code, no typed error — so keep it narrow
53
+ * enough that a different failure never claims the retry.
54
+ */
55
+ export declare function isMissingSessionError(err: unknown): boolean;
41
56
  /**
42
57
  * A Ziggs agent whose brain is the Claude Agent SDK. Same host, same `eff`
43
58
  * side channel, same tool surface as the FSM `Agent` — the ONLY difference is
@@ -56,6 +71,13 @@ export declare class ClaudeSdkAgent {
56
71
  private maxTurns;
57
72
  private statelessPerWake;
58
73
  private queryFn;
74
+ /**
75
+ * ZIG-1120 #7 — tool set is fixed at host construction; rebuilding the
76
+ * in-process MCP server (+ zod schema tree) every wake was pure waste.
77
+ * A mutable holder supplies per-wake context without closing over `tick`.
78
+ */
79
+ private toolContextHolder;
80
+ private sdkMcpServer;
59
81
  constructor(opts: ClaudeSdkAgentOptions);
60
82
  tick({ event, ctx, state, identity, eff, signal }: TickInput): Promise<TickOutput>;
61
83
  }