@ziggs-ai/agent-sdk 0.16.1 → 0.17.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (113) hide show
  1. package/CHANGELOG.md +10 -0
  2. package/README.md +110 -88
  3. package/dist/{AgentHost.d.ts → Agent.d.ts} +50 -27
  4. package/dist/{AgentHost.js → Agent.js} +135 -129
  5. package/dist/adapters/AnthropicAdapter.d.ts +1 -1
  6. package/dist/adapters/AnthropicAdapter.js +1 -1
  7. package/dist/{agent/ClaudeSdkAgent.d.ts → brain/ClaudeBrain.d.ts} +23 -11
  8. package/dist/{agent/ClaudeSdkAgent.js → brain/ClaudeBrain.js} +99 -99
  9. package/dist/{agent/Agent.d.ts → brain/ZiggsBrain.d.ts} +23 -20
  10. package/dist/{agent/Agent.js → brain/ZiggsBrain.js} +12 -11
  11. package/dist/{agent → brain}/claudeCognition.d.ts +1 -1
  12. package/dist/{agent → brain}/claudeCognition.js +1 -1
  13. package/dist/{agent → brain}/claudeSdkProtocol.d.ts +1 -1
  14. package/dist/{agent → brain}/claudeSdkProtocol.js +1 -1
  15. package/dist/{agent → brain}/claudeSdkTools.d.ts +1 -1
  16. package/dist/{agent → brain}/claudeSdkTools.js +5 -14
  17. package/dist/{agent → brain}/hostedAgentConnection.d.ts +1 -1
  18. package/dist/{agent → brain}/hostedAgentConnection.js +3 -3
  19. package/dist/brain/index.d.ts +6 -6
  20. package/dist/brain/index.js +2 -2
  21. package/dist/brain/onBehalfAgreement.d.ts +51 -0
  22. package/dist/brain/onBehalfAgreement.js +99 -0
  23. package/dist/{agent → brain}/toolManagerToSdkMcp.js +10 -1
  24. package/dist/defineAgent.js +4 -4
  25. package/dist/formatters/HistoryFormatter.js +2 -1
  26. package/dist/index.d.ts +30 -31
  27. package/dist/index.js +24 -25
  28. package/dist/{adapters/McpAdapter.d.ts → mcp/createMcpAgent.d.ts} +16 -15
  29. package/dist/{adapters/McpAdapter.js → mcp/createMcpAgent.js} +54 -40
  30. package/dist/memory/MemoryStore.d.ts +18 -1
  31. package/dist/memory/MemoryStore.js +43 -0
  32. package/dist/{server → platform}/ConnectionPool.d.ts +9 -3
  33. package/dist/{server → platform}/ConnectionPool.js +16 -4
  34. package/dist/{server → platform}/EventQueue.d.ts +1 -1
  35. package/dist/{server → platform}/StateTimers.d.ts +1 -1
  36. package/dist/{server → platform}/StateTimers.js +1 -1
  37. package/dist/{server → platform}/ZiggsEffectHandler.d.ts +4 -4
  38. package/dist/{server → platform}/ZiggsEffectHandler.js +2 -2
  39. package/dist/{server → platform}/inboxLoop.d.ts +34 -1
  40. package/dist/{server → platform}/inboxLoop.js +122 -14
  41. package/dist/{server → platform}/polls/PollKind.d.ts +4 -7
  42. package/dist/{server → platform}/polls/PollKind.js +3 -6
  43. package/dist/{server → platform}/polls/PollRunner.d.ts +5 -6
  44. package/dist/{server → platform}/polls/PollRunner.js +24 -12
  45. package/dist/platform/polls/declaredPollRunner.d.ts +4 -0
  46. package/dist/platform/polls/declaredPollRunner.js +29 -0
  47. package/dist/{server → platform}/tools/tier2/mcpConnectionTools.js +6 -3
  48. package/dist/runtime/bootstrapCtx.d.ts +1 -1
  49. package/dist/runtime/bootstrapCtx.js +1 -1
  50. package/dist/runtime/buildOutcome.js +1 -1
  51. package/dist/runtime/runTurn.d.ts +1 -1
  52. package/dist/runtime/runTurn.js +18 -18
  53. package/dist/runtime/validateWorkflow.js +1 -1
  54. package/dist/skills/parseSkill.d.ts +7 -2
  55. package/dist/skills/parseSkill.js +22 -5
  56. package/dist/skills/skillReadReference.d.ts +6 -0
  57. package/dist/skills/skillReadReference.js +27 -0
  58. package/dist/tools/needs.js +2 -2
  59. package/dist/tools/toolCreds.d.ts +1 -1
  60. package/dist/tools/toolCreds.js +1 -1
  61. package/dist/types.d.ts +3 -3
  62. package/package.json +2 -2
  63. package/dist/server/createHealthServer.d.ts +0 -7
  64. package/dist/server/createHealthServer.js +0 -75
  65. package/dist/server/runLauncher.d.ts +0 -22
  66. package/dist/server/runLauncher.js +0 -160
  67. /package/dist/{agent → brain}/toolManagerToSdkMcp.d.ts +0 -0
  68. /package/dist/{server → platform}/EventQueue.js +0 -0
  69. /package/dist/{server → platform}/OutboxBuffer.d.ts +0 -0
  70. /package/dist/{server → platform}/OutboxBuffer.js +0 -0
  71. /package/dist/{server → platform}/SeenMessages.d.ts +0 -0
  72. /package/dist/{server → platform}/SeenMessages.js +0 -0
  73. /package/dist/{server → platform}/agreements/AgreementService.d.ts +0 -0
  74. /package/dist/{server → platform}/agreements/AgreementService.js +0 -0
  75. /package/dist/{server → platform}/proactive/ProactiveTrigger.d.ts +0 -0
  76. /package/dist/{server → platform}/proactive/ProactiveTrigger.js +0 -0
  77. /package/dist/{server → platform}/requestMatch.d.ts +0 -0
  78. /package/dist/{server → platform}/requestMatch.js +0 -0
  79. /package/dist/{server → platform}/tasks/TaskService.d.ts +0 -0
  80. /package/dist/{server → platform}/tasks/TaskService.js +0 -0
  81. /package/dist/{server → platform}/tasks/index.d.ts +0 -0
  82. /package/dist/{server → platform}/tasks/index.js +0 -0
  83. /package/dist/{server → platform}/telemetryIngest.d.ts +0 -0
  84. /package/dist/{server → platform}/telemetryIngest.js +0 -0
  85. /package/dist/{server → platform}/tools/index.d.ts +0 -0
  86. /package/dist/{server → platform}/tools/index.js +0 -0
  87. /package/dist/{server → platform}/tools/tier1/protocolRunner.d.ts +0 -0
  88. /package/dist/{server → platform}/tools/tier1/protocolRunner.js +0 -0
  89. /package/dist/{server → platform}/tools/tier1/protocolTools.d.ts +0 -0
  90. /package/dist/{server → platform}/tools/tier1/protocolTools.js +0 -0
  91. /package/dist/{server → platform}/tools/tier2/artifactTools.d.ts +0 -0
  92. /package/dist/{server → platform}/tools/tier2/artifactTools.js +0 -0
  93. /package/dist/{server → platform}/tools/tier2/chatTools.d.ts +0 -0
  94. /package/dist/{server → platform}/tools/tier2/chatTools.js +0 -0
  95. /package/dist/{server → platform}/tools/tier2/connectionTools.d.ts +0 -0
  96. /package/dist/{server → platform}/tools/tier2/connectionTools.js +0 -0
  97. /package/dist/{server → platform}/tools/tier2/contextTools.d.ts +0 -0
  98. /package/dist/{server → platform}/tools/tier2/contextTools.js +0 -0
  99. /package/dist/{server → platform}/tools/tier2/discoveryTools.d.ts +0 -0
  100. /package/dist/{server → platform}/tools/tier2/discoveryTools.js +0 -0
  101. /package/dist/{server → platform}/tools/tier2/grantsTools.d.ts +0 -0
  102. /package/dist/{server → platform}/tools/tier2/grantsTools.js +0 -0
  103. /package/dist/{server → platform}/tools/tier2/introductionTools.d.ts +0 -0
  104. /package/dist/{server → platform}/tools/tier2/introductionTools.js +0 -0
  105. /package/dist/{server → platform}/tools/tier2/linkTools.d.ts +0 -0
  106. /package/dist/{server → platform}/tools/tier2/linkTools.js +0 -0
  107. /package/dist/{server → platform}/tools/tier2/marketplaceTools.d.ts +0 -0
  108. /package/dist/{server → platform}/tools/tier2/marketplaceTools.js +0 -0
  109. /package/dist/{server → platform}/tools/tier2/mcpConnectionTools.d.ts +0 -0
  110. /package/dist/{server → platform}/tools/tier2/paymentTools.d.ts +0 -0
  111. /package/dist/{server → platform}/tools/tier2/paymentTools.js +0 -0
  112. /package/dist/{server → platform}/tools/tier2/taskTools.d.ts +0 -0
  113. /package/dist/{server → platform}/tools/tier2/taskTools.js +0 -0
@@ -8,6 +8,62 @@ function log(label, msg) {
8
8
  function warn(label, msg) {
9
9
  runtimeLog.warn(label, msg);
10
10
  }
11
+ /**
12
+ * Store publish waits on a `probe_*` chat reply, not inbox ack. Act those
13
+ * rooms before leftover mail so a standing host answers the gate even when
14
+ * the envelope also carries older chats.
15
+ */
16
+ export function chatsInWakeOrder(chats) {
17
+ return [...chats].sort((a, b) => {
18
+ const ap = isPublishProbeChat(a.chatId) ? 0 : 1;
19
+ const bp = isPublishProbeChat(b.chatId) ? 0 : 1;
20
+ if (ap !== bp)
21
+ return ap - bp;
22
+ return a.latestAt < b.latestAt ? 1 : -1;
23
+ });
24
+ }
25
+ function isPublishProbeChat(chatId) {
26
+ return chatId.startsWith('probe_');
27
+ }
28
+ /**
29
+ * Store publish waits on a chat reply, not inbox ack. The ordinary pass can
30
+ * already be inside leftover mail when that room appears, so a side read
31
+ * answers `probe_*` chats without waiting for the current pass to finish.
32
+ * It never acks — the main pass still owns the watermark.
33
+ */
34
+ export async function answerPublishProbes(deps) {
35
+ const inbox = new InboxClient(deps.creds.operatorKey, deps.creds.agentId, deps.baseUrl);
36
+ const reader = new ContextReadClient(deps.creds.operatorKey, deps.creds.agentId, deps.baseUrl);
37
+ const envelope = await inbox.getInbox({});
38
+ const probes = (envelope.chats ?? []).filter((c) => isPublishProbeChat(c.chatId));
39
+ if (probes.length === 0)
40
+ return 0;
41
+ const mine = (d) => d.assigneeId === deps.ownAgentId;
42
+ const earliestByChat = new Map();
43
+ for (const d of envelope.deliveries) {
44
+ if (!d.chatId || !mine(d) || !isPublishProbeChat(d.chatId))
45
+ continue;
46
+ const seen = earliestByChat.get(d.chatId);
47
+ if (!seen || d.ts < seen)
48
+ earliestByChat.set(d.chatId, d.ts);
49
+ }
50
+ const attempt = async (what, handover) => {
51
+ try {
52
+ await handover();
53
+ return true;
54
+ }
55
+ catch (err) {
56
+ warn(deps.label ?? 'inboxLoop', `publish-probe ${what} was not handled: ${err?.message ?? err}`);
57
+ return false;
58
+ }
59
+ };
60
+ let delivered = 0;
61
+ for (const news of probes) {
62
+ const earliest = earliestByChat.get(news.chatId) ?? news.latestAt;
63
+ delivered += await deliverMessagesForChat(deps, news, earliest, reader, attempt);
64
+ }
65
+ return delivered;
66
+ }
11
67
  /**
12
68
  * Standing proposals share the merged inbox visibility model with deliveries,
13
69
  * but older envelopes predate an explicit assignee stamp. Prefer the server's
@@ -116,9 +172,9 @@ export async function pollInboxOnce(deps, waitSeconds = 0, opts = {}) {
116
172
  }
117
173
  };
118
174
  if (envelope.deliveriesCapped) {
119
- // Informational only — does NOT block acking what WAS handed over. The
120
- // tail stays unacked and arrives on the next pass; never acking here would
121
- // make a badly backed-up agent replay its whole backlog forever.
175
+ // The listed window is the oldest CAP. Acking it slides the cursor; the
176
+ // newer tail stays past ackTo and arrives on the next pass. Holding here
177
+ // was the wedge: the same over-cap window came back forever.
122
178
  warn(label, 'inbox deliveriesCapped (more mail than one envelope carries — the tail follows on the next pass)');
123
179
  }
124
180
  // The single-assign law: a row is MINE to act on iff it carries my stamp.
@@ -136,7 +192,7 @@ export async function pollInboxOnce(deps, waitSeconds = 0, opts = {}) {
136
192
  if (!seen || d.ts < seen)
137
193
  earliestByChat.set(d.chatId, d.ts);
138
194
  }
139
- for (const news of envelope.chats) {
195
+ for (const news of chatsInWakeOrder(envelope.chats ?? [])) {
140
196
  const earliest = earliestByChat.get(news.chatId) ?? news.latestAt;
141
197
  const n = await deliverMessagesForChat(deps, news, earliest, reader, attempt);
142
198
  messagesDelivered += n;
@@ -195,7 +251,7 @@ export async function pollInboxOnce(deps, waitSeconds = 0, opts = {}) {
195
251
  }
196
252
  // request channel. Triage is the host's job (exact string compare);
197
253
  // a miss must still count as handled so the watermark can advance with 0 tokens.
198
- for (const request of envelope.requestsAwaitingMe ?? []) {
254
+ for (const request of envelope.openRequestsAwaitingMe ?? []) {
199
255
  const key = `request:${request.agreementId}:${request.ts}`;
200
256
  if (deps.seen.has(key))
201
257
  continue;
@@ -245,12 +301,7 @@ export async function pollInboxOnce(deps, waitSeconds = 0, opts = {}) {
245
301
  // this window has failed too many times in a row and is now poison, in which
246
302
  // case it is dropped loudly rather than wedging the agent forever.
247
303
  let acked = false;
248
- // A capped envelope has unlisted older deliveries behind ackTo;
249
- // acking would refuse or bury them. Hold until a later uncapped pass.
250
- if (envelope.deliveriesCapped && envelope.ackTo) {
251
- warn(label, `holding the watermark at ${envelope.ackTo} — deliveriesCapped; ack would not cover the unlisted tail`);
252
- }
253
- else if (envelope.ackTo && (failures === 0 || opts.forceAck)) {
304
+ if (envelope.ackTo && (failures === 0 || opts.forceAck)) {
254
305
  if (failures > 0) {
255
306
  runtimeLog.error(label, `acking a window that failed ${failures} handover(s) — giving up on redelivery to keep the ` +
256
307
  `agent alive; this mail is lost (ackTo=${envelope.ackTo})`);
@@ -261,7 +312,7 @@ export async function pollInboxOnce(deps, waitSeconds = 0, opts = {}) {
261
312
  const handledResourceIds = [
262
313
  ...new Set([
263
314
  ...envelope.deliveries.filter(mine).map((d) => d.resourceId),
264
- ...(envelope.requestsAwaitingMe ?? []).map((q) => q.agreementId),
315
+ ...(envelope.openRequestsAwaitingMe ?? []).map((q) => q.agreementId),
265
316
  ]),
266
317
  ].filter((id) => typeof id === 'string' && id.length > 0);
267
318
  await inbox.ack(envelope.ackTo, { handledResourceIds });
@@ -289,6 +340,16 @@ export async function pollInboxOnce(deps, waitSeconds = 0, opts = {}) {
289
340
  * some defaults ship with. Lower it if your deployment sits behind a shorter one.
290
341
  */
291
342
  const DEFAULT_WAIT_SECONDS = 110;
343
+ /**
344
+ * Side-poll interval for store-publish `probe_*` rooms.
345
+ *
346
+ * Default **off**. A second GET /inbox beside the main long-poll burned the
347
+ * shared poll ceiling: every hosted agent side-polled, probes got 429s, and
348
+ * Register saw ALB HTML 504s. Main pass + probe-first ordering + pinned
349
+ * `probe_*` deliveries are enough; set `probePollMs` only when you knowingly
350
+ * need the extra loop.
351
+ */
352
+ const DEFAULT_PROBE_POLL_MS = 0;
292
353
  const ERROR_BACKOFF_BASE_MS = 1_000;
293
354
  const ERROR_BACKOFF_MAX_MS = 30_000;
294
355
  /**
@@ -299,6 +360,14 @@ const ERROR_BACKOFF_MAX_MS = 30_000;
299
360
  * rather than an unbounded loop.
300
361
  */
301
362
  const MAX_HELD_PASSES = 3;
363
+ /**
364
+ * How many consecutive passes have refused to advance the watermark.
365
+ * A capped envelope used to reset the counter (zero handover failures),
366
+ * so `forceAck` never fired on the one hold that lasts forever.
367
+ */
368
+ export function heldPassAfter(prev, pass) {
369
+ return !pass.acked && (pass.failures > 0 || pass.truncated) ? prev + 1 : 0;
370
+ }
302
371
  function sleep(ms) {
303
372
  return new Promise((resolve) => setTimeout(resolve, ms));
304
373
  }
@@ -317,21 +386,28 @@ function sleep(ms) {
317
386
  export function runInboxLoop(deps, opts = {}) {
318
387
  const label = deps.label ?? 'inboxLoop';
319
388
  const waitSeconds = opts.waitSeconds ?? DEFAULT_WAIT_SECONDS;
389
+ const probePollMs = opts.probePollMs ?? DEFAULT_PROBE_POLL_MS;
320
390
  let running = true;
321
391
  let consecutiveErrors = 0;
322
392
  let heldPasses = 0;
393
+ let lastPassAt = null;
394
+ let lastAttemptAt = null;
323
395
  let first = true;
324
- const done = (async () => {
396
+ const main = (async () => {
325
397
  while (running) {
326
398
  try {
399
+ lastAttemptAt = Date.now();
327
400
  const pass = await pollInboxOnce(deps, first ? 0 : waitSeconds, {
328
401
  forceAck: heldPasses >= MAX_HELD_PASSES,
329
402
  });
403
+ lastPassAt = Date.now();
330
404
  first = false;
331
405
  consecutiveErrors = 0;
332
406
  // A held pass means the same window comes back; count how many times we
333
407
  // have refused it so a permanently unhandleable frame cannot spin here.
334
- heldPasses = pass.failures > 0 && !pass.acked ? heldPasses + 1 : 0;
408
+ // Capped holds used to reset this: failures === 0, so the poison
409
+ // escape never climbed. Count any pass that refused to advance.
410
+ heldPasses = heldPassAfter(heldPasses, pass);
335
411
  }
336
412
  catch (err) {
337
413
  if (!running)
@@ -356,10 +432,42 @@ export function runInboxLoop(deps, opts = {}) {
356
432
  }
357
433
  }
358
434
  })();
435
+ const probes = (async () => {
436
+ if (probePollMs <= 0)
437
+ return;
438
+ while (running) {
439
+ const slice = 50;
440
+ let left = probePollMs;
441
+ while (left > 0 && running) {
442
+ const step = Math.min(slice, left);
443
+ await sleep(step);
444
+ left -= step;
445
+ }
446
+ if (!running)
447
+ break;
448
+ try {
449
+ const n = await answerPublishProbes(deps);
450
+ if (n > 0)
451
+ log(label, `publish-probe side poll delivered ${n} message(s)`);
452
+ }
453
+ catch (err) {
454
+ if (!running)
455
+ break;
456
+ warn(label, `publish-probe side poll failed: ${err?.message ?? err}`);
457
+ }
458
+ }
459
+ })();
460
+ const done = Promise.all([main, probes]);
359
461
  return {
360
462
  get running() {
361
463
  return running;
362
464
  },
465
+ get lastPassAt() {
466
+ return lastPassAt;
467
+ },
468
+ get lastAttemptAt() {
469
+ return lastAttemptAt;
470
+ },
363
471
  async stop() {
364
472
  running = false;
365
473
  await done.catch(() => { });
@@ -5,12 +5,9 @@
5
5
  *
6
6
  * polls: [{ kind: 'sente-email', identityId: 'idt_...' }]
7
7
  *
8
- * It declares; it does not execute. Execution belongs to PollRunner, which the
9
- * launcher runs once for the whole fleet — polling is a fleet concern the way
10
- * the health server is, not a per-host behaviour: one loop per (agent, binding)
11
- * with its own interval, backoff and backpressure, instead of every host
12
- * hand-rolling a timer inside its message loop. Declaration travels with the
13
- * agent; execution does not.
8
+ * The declaration and its PollRunner travel with the Agent. connectAsync starts
9
+ * both inbox delivery and external polls; disconnect stops both. The process
10
+ * runner does not own a second copy of the polling lifecycle.
14
11
  *
15
12
  * A poll answers ONE question: is there work for this agent right now. It never
16
13
  * returns the work itself. The agent reads that for itself once woken, through
@@ -26,7 +23,7 @@
26
23
  * Kind-specific config, verbatim from the agent's manifest.
27
24
  *
28
25
  * `chatId` is not kind-specific and is not optional in practice: a wake has to
29
- * resolve to a lane or AgentHost's session resolver returns null and drops it
26
+ * resolve to a lane or Agent's session resolver returns null and drops it
30
27
  * silently. A poll has no engagement behind it to derive one from, so the
31
28
  * binding names the chat the agent thinks in — which also puts its triage
32
29
  * somewhere a human can watch. PollRunner refuses to register a binding without
@@ -5,12 +5,9 @@
5
5
  *
6
6
  * polls: [{ kind: 'sente-email', identityId: 'idt_...' }]
7
7
  *
8
- * It declares; it does not execute. Execution belongs to PollRunner, which the
9
- * launcher runs once for the whole fleet — polling is a fleet concern the way
10
- * the health server is, not a per-host behaviour: one loop per (agent, binding)
11
- * with its own interval, backoff and backpressure, instead of every host
12
- * hand-rolling a timer inside its message loop. Declaration travels with the
13
- * agent; execution does not.
8
+ * The declaration and its PollRunner travel with the Agent. connectAsync starts
9
+ * both inbox delivery and external polls; disconnect stops both. The process
10
+ * runner does not own a second copy of the polling lifecycle.
14
11
  *
15
12
  * A poll answers ONE question: is there work for this agent right now. It never
16
13
  * returns the work itself. The agent reads that for itself once woken, through
@@ -1,11 +1,9 @@
1
1
  /**
2
- * PollRunner — executes declared polls for a whole fleet, outside any host.
2
+ * PollRunner — executes declared external wake sources for agents.
3
3
  *
4
- * Every host already long-polls its own Ziggs inbox; this applies the same wake
5
- * discipline to the sources Ziggs cannot see — mailboxes, feeds, whatever a
6
- * PollKind can ask. It runs in the launcher rather than inside any AgentHost
7
- * because none of it belongs to a host's message loop: a poll is per
8
- * (agent, binding) and carries its own interval, backoff and backpressure.
4
+ * Each Agent owns its runner and starts it with its inbox loop. A poll is per
5
+ * (agent, binding), with its own interval, backoff and backpressure. This same
6
+ * lifecycle applies to fleet, MCP-tool, lab and evaluation agents.
9
7
  *
10
8
  * One loop per (agent, binding), and each loop AWAITS its own wake before
11
9
  * scheduling the next tick. That is the whole backpressure story: while an
@@ -73,6 +71,7 @@ export declare class PollRunner {
73
71
  private readonly wakeWindowMs;
74
72
  private readonly loops;
75
73
  private running;
74
+ private generation;
76
75
  constructor({ pool, registry, agents, operatorKey, defaultIntervalMs, maxBackoffMs, wakeTimeoutMs, maxWakesPerWindow, wakeWindowMs, label, }: PollRunnerOptions);
77
76
  get size(): number;
78
77
  start(): void;
@@ -1,11 +1,9 @@
1
1
  /**
2
- * PollRunner — executes declared polls for a whole fleet, outside any host.
2
+ * PollRunner — executes declared external wake sources for agents.
3
3
  *
4
- * Every host already long-polls its own Ziggs inbox; this applies the same wake
5
- * discipline to the sources Ziggs cannot see — mailboxes, feeds, whatever a
6
- * PollKind can ask. It runs in the launcher rather than inside any AgentHost
7
- * because none of it belongs to a host's message loop: a poll is per
8
- * (agent, binding) and carries its own interval, backoff and backpressure.
4
+ * Each Agent owns its runner and starts it with its inbox loop. A poll is per
5
+ * (agent, binding), with its own interval, backoff and backpressure. This same
6
+ * lifecycle applies to fleet, MCP-tool, lab and evaluation agents.
9
7
  *
10
8
  * One loop per (agent, binding), and each loop AWAITS its own wake before
11
9
  * scheduling the next tick. That is the whole backpressure story: while an
@@ -30,6 +28,7 @@ export class PollRunner {
30
28
  wakeWindowMs;
31
29
  loops = [];
32
30
  running = false;
31
+ generation = 0;
33
32
  constructor({ pool, registry, agents, operatorKey, defaultIntervalMs = 60_000, maxBackoffMs = 15 * 60_000, wakeTimeoutMs = Number(process.env.CLAUDE_SDK_TICK_TIMEOUT_MS ?? 300_000), maxWakesPerWindow = 20, wakeWindowMs = 60 * 60_000, label = 'PollRunner', }) {
34
33
  this.pool = pool;
35
34
  this.operatorKey = operatorKey;
@@ -65,6 +64,7 @@ export class PollRunner {
65
64
  chatId: binding.chatId,
66
65
  timer: null,
67
66
  failures: 0,
67
+ inFlight: false,
68
68
  wakes: 0,
69
69
  windowStartedAt: Date.now(),
70
70
  capped: false,
@@ -79,14 +79,18 @@ export class PollRunner {
79
79
  if (this.running)
80
80
  return;
81
81
  this.running = true;
82
+ this.generation++;
82
83
  if (!this.loops.length)
83
84
  return;
84
- for (const loop of this.loops)
85
- this.schedule(loop, 0);
85
+ for (const loop of this.loops) {
86
+ if (!loop.inFlight)
87
+ this.schedule(loop, 0);
88
+ }
86
89
  runtimeLog.info(this.label, `started — ${this.loops.length} poll(s): ${this.loops.map((l) => `${l.agentId}:${l.kind.id}`).join(', ')}`);
87
90
  }
88
91
  stop() {
89
92
  this.running = false;
93
+ this.generation++;
90
94
  for (const loop of this.loops) {
91
95
  if (loop.timer)
92
96
  clearTimeout(loop.timer);
@@ -97,7 +101,14 @@ export class PollRunner {
97
101
  schedule(loop, delayMs = this.nextDelay(loop)) {
98
102
  if (!this.running)
99
103
  return;
100
- loop.timer = setTimeout(() => void this.tick(loop), delayMs);
104
+ loop.timer = setTimeout(() => {
105
+ loop.timer = null;
106
+ loop.inFlight = true;
107
+ void this.tick(loop, this.generation).finally(() => {
108
+ loop.inFlight = false;
109
+ this.schedule(loop);
110
+ });
111
+ }, delayMs);
101
112
  }
102
113
  /** Interval while healthy; doubling up to the ceiling while failing. */
103
114
  nextDelay(loop) {
@@ -105,7 +116,7 @@ export class PollRunner {
105
116
  return loop.intervalMs;
106
117
  return Math.min(loop.intervalMs * 2 ** loop.failures, this.maxBackoffMs);
107
118
  }
108
- async tick(loop) {
119
+ async tick(loop, generation = this.generation) {
109
120
  if (!this.running)
110
121
  return;
111
122
  let result;
@@ -119,9 +130,11 @@ export class PollRunner {
119
130
  catch (err) {
120
131
  loop.failures += 1;
121
132
  runtimeLog.error(this.label, `${loop.agentId}:${loop.kind.id} source failed (${loop.failures}x, next in ${this.nextDelay(loop)}ms): ${err.message}`);
122
- this.schedule(loop);
123
133
  return;
124
134
  }
135
+ // A source read may finish after disconnect or a new connection.
136
+ if (!this.running || generation !== this.generation)
137
+ return;
125
138
  if (result?.actionable && this.claimWake(loop)) {
126
139
  const hint = result.hint ?? `New work from ${loop.kind.id}. Check it.`;
127
140
  try {
@@ -140,7 +153,6 @@ export class PollRunner {
140
153
  runtimeLog.error(this.label, `${loop.agentId} wake failed (${loop.failures}x, next in ${this.nextDelay(loop)}ms): ${err.message}`);
141
154
  }
142
155
  }
143
- this.schedule(loop);
144
156
  }
145
157
  /**
146
158
  * Take one wake from this binding's budget, or refuse.
@@ -0,0 +1,4 @@
1
+ import type { AgentOptions } from '../../Agent.js';
2
+ import { PollRunner, type WakeTarget } from './PollRunner.js';
3
+ /** Validate before starting any work, so a declared wake source cannot vanish. */
4
+ export declare function createDeclaredPollRunner(options: AgentOptions, target: WakeTarget): PollRunner | null;
@@ -0,0 +1,29 @@
1
+ import { PollRegistry } from './PollKind.js';
2
+ import { PollRunner } from './PollRunner.js';
3
+ /** Validate before starting any work, so a declared wake source cannot vanish. */
4
+ export function createDeclaredPollRunner(options, target) {
5
+ const registry = new PollRegistry();
6
+ for (const kind of options.pollKinds ?? [])
7
+ registry.register(kind);
8
+ if (options.polls === undefined)
9
+ return null;
10
+ if (!Array.isArray(options.polls))
11
+ throw new Error('Agent: polls must be an array');
12
+ for (const binding of options.polls) {
13
+ if (!binding || !registry.get(binding.kind)) {
14
+ throw new Error(`Agent ${options.agentId}: unknown poll kind "${binding?.kind}"; declare its implementation in pollKinds`);
15
+ }
16
+ if (typeof binding.chatId !== 'string' || !binding.chatId.trim()) {
17
+ throw new Error(`Agent ${options.agentId}: poll "${binding.kind}" requires a chatId`);
18
+ }
19
+ }
20
+ if (!options.polls.length)
21
+ return null;
22
+ return new PollRunner({
23
+ pool: target,
24
+ registry,
25
+ agents: [{ agentId: options.agentId, polls: options.polls }],
26
+ operatorKey: options.operatorKey,
27
+ label: `${options.name || options.agentId}-polls`,
28
+ });
29
+ }
@@ -2,7 +2,7 @@ import { Client } from '@modelcontextprotocol/sdk/client/index.js';
2
2
  import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js';
3
3
  import { ConnectionsClient } from '@ziggs-ai/api-client';
4
4
  import { defineTool } from '../../../tools/defineTool.js';
5
- import { resolveZiggsGatewayTarget } from '../../../adapters/McpAdapter.js';
5
+ import { resolveZiggsGatewayTarget } from '../../../mcp/createMcpAgent.js';
6
6
  /**
7
7
  * Calling a tool on a connected MCP server, one shot.
8
8
  *
@@ -15,7 +15,7 @@ import { resolveZiggsGatewayTarget } from '../../../adapters/McpAdapter.js';
15
15
  *
16
16
  * The gateway (`POST /connections/:id/mcp`) has always been the right door and is
17
17
  * already in production; what was missing was a tool that walks through it. The
18
- * transport, headers and MCP handshake are the same ones `McpAdapter`'s `ziggs`
18
+ * transport, headers and MCP handshake are the same ones `createMcpAgent`'s `ziggs`
19
19
  * transport uses — reused rather than re-derived, so a change to the gateway
20
20
  * contract lands in one place.
21
21
  *
@@ -66,7 +66,10 @@ async function withClient(ctx, connectionId, grantId, run) {
66
66
  grantId,
67
67
  operatorKey: ctx['operatorKey'],
68
68
  agentId: ctx['agentId'],
69
- }, { agentId: ctx['agentId'] });
69
+ }, {
70
+ agentId: ctx['agentId'],
71
+ laneId: ctx['laneId'],
72
+ });
70
73
  const transport = new StreamableHTTPClientTransport(target.url, {
71
74
  requestInit: { headers: target.headers },
72
75
  });
@@ -3,7 +3,7 @@ import type { Ctx, Identity } from '../types.js';
3
3
  * The one place a per-lane `Ctx` is constructed.
4
4
  *
5
5
  * Both cognitions bootstrap the same Ctx: the FSM (`AgentMachine` constructor)
6
- * and the Claude Agent SDK brain (`ClaudeSdkAgent.tick`). Before this, each
6
+ * and the Claude Agent SDK brain (`ClaudeBrain.tick`). Before this, each
7
7
  * carried its own copy of the fresh-Ctx literal and the restore-path defaulting,
8
8
  * and they had already drifted (the "default them ONCE here" comment in
9
9
  * AgentMachine no longer held). Keep it here so the shape lives in a single spot.
@@ -3,7 +3,7 @@ import { migrateLegacyTaskFields } from './taskBookkeeping.js';
3
3
  * The one place a per-lane `Ctx` is constructed.
4
4
  *
5
5
  * Both cognitions bootstrap the same Ctx: the FSM (`AgentMachine` constructor)
6
- * and the Claude Agent SDK brain (`ClaudeSdkAgent.tick`). Before this, each
6
+ * and the Claude Agent SDK brain (`ClaudeBrain.tick`). Before this, each
7
7
  * carried its own copy of the fresh-Ctx literal and the restore-path defaulting,
8
8
  * and they had already drifted (the "default them ONCE here" comment in
9
9
  * AgentMachine no longer held). Keep it here so the shape lives in a single spot.
@@ -162,7 +162,7 @@ function singleEventToOutcome(ev, ownAgentId) {
162
162
  return null;
163
163
  }
164
164
  if (ev.type === 'state-timeout') {
165
- // Synthetic frame from AgentHost's StateTimers — host-constructed, fields always present.
165
+ // Synthetic frame from Agent's StateTimers — host-constructed, fields always present.
166
166
  return { kind: 'state-timeout', state: ev.state, waitedMs: ev.waitedMs };
167
167
  }
168
168
  // Only real inbound chat messages should drive the FSM. Backend bookkeeping
@@ -15,7 +15,7 @@ interface RunTurnInput {
15
15
  toolManager: ToolManager;
16
16
  promptBuilder: PromptBuilder;
17
17
  /**
18
- * Agent-private long-term memory, threaded from AgentHost. Currently
18
+ * Agent-private long-term memory, threaded from Agent. Currently
19
19
  * surfaced into `eff` as part of the tool-call effect payload so tools
20
20
  * can read/write notes; the prompt builder doesn't consume it yet.
21
21
  */
@@ -1,7 +1,9 @@
1
+ import { isAgreementId } from '@ziggs-ai/api-client';
1
2
  import { outcomeFromActionResult } from './buildOutcome.js';
2
3
  import { runtimeLog } from '../shared/runtimeLog.js';
3
4
  import { safeParseJSON } from '../utils/jsonExtractor.js';
4
5
  import { withDeclaredMandate } from './mandateDeclaration.js';
6
+ import { resolveOnBehalfAgreementId, } from '../brain/onBehalfAgreement.js';
5
7
  const MAX_TOOL_LOOP_ITERS = 20;
6
8
  const MAX_MSG_CHARS = 3500;
7
9
  /**
@@ -117,9 +119,7 @@ export async function runTurn(input) {
117
119
  // only. a bare agreement id as sessionId is a mis-keyed lane —
118
120
  // loud-log the empty snapshot rather than staying silent.
119
121
  if (ctxObj.chatMissing) {
120
- const looksLikeBareAgreement = typeof sessionId === 'string' &&
121
- sessionId.startsWith('agr_') &&
122
- !sessionId.startsWith('agrn-');
122
+ const looksLikeBareAgreement = isAgreementId(sessionId);
123
123
  if (looksLikeBareAgreement) {
124
124
  runtimeLog.warn('runTurn', `chatMissing on non-chat sessionId=${sessionId} (state=${stateId}) — ` +
125
125
  `lane was keyed by agreementId instead of the agreement's origin/notification chat; ` +
@@ -265,7 +265,9 @@ export async function runTurn(input) {
265
265
  // not. The runtime knows; the model would have to be told.
266
266
  args = withDeclaredMandate(args, toolName, toolManager, ctx.activeAgreementId);
267
267
  const decision = { action: matchingAction, tool: toolName, args };
268
- const result = await executeDecision(decision, actionDef, effWithTrace, sessionId, memory, null, addToolUsage);
268
+ const result = await executeDecision(decision, actionDef, effWithTrace, sessionId, memory, incomingEvent, addToolUsage, ctx.identity.agentId, Array.isArray(ctxObj.agreements)
269
+ ? ctxObj.agreements
270
+ : null);
269
271
  toolCallResults.push({ tc, matchingAction, result, args, actionDef });
270
272
  }
271
273
  let hasTerminal = false;
@@ -418,7 +420,9 @@ export async function runTurn(input) {
418
420
  try {
419
421
  // effWithTrace, not eff: JSON-path tool calls must carry runId/stateId
420
422
  // and the wake's write keys exactly like tool-loop calls do.
421
- result = await executeDecision(decision, actionDef, effWithTrace, sessionId, memory, null, addToolUsage);
423
+ result = await executeDecision(decision, actionDef, effWithTrace, sessionId, memory, incomingEvent, addToolUsage, ctx.identity.agentId, Array.isArray(ctxObj.agreements)
424
+ ? ctxObj.agreements
425
+ : null);
422
426
  }
423
427
  catch (err) {
424
428
  // Only outbound sends can throw out of executeDecision — executeTool
@@ -642,17 +646,18 @@ function findActionForTool(toolName, allowedActionNames, actions) {
642
646
  }
643
647
  return null;
644
648
  }
645
- async function executeDecision(decision, actionDef, eff, sessionId, memory, incomingEvent = null, onUsage) {
649
+ async function executeDecision(decision, actionDef, eff, sessionId, memory, incomingEvent = null, onUsage, agentId = null, agreements = null) {
646
650
  const boundTool = actionDef?.tool || null;
647
651
  if (boundTool) {
648
652
  decision.tool = boundTool;
649
- return await executeTool(decision, eff, sessionId, memory, incomingEvent, onUsage);
653
+ return await executeTool(decision, eff, sessionId, memory, incomingEvent, onUsage, agentId, agreements);
650
654
  }
651
655
  if (decision.action === '_genericDiscoveryTool' && decision.tool) {
652
- return await executeTool(decision, eff, sessionId, memory, incomingEvent, onUsage);
656
+ return await executeTool(decision, eff, sessionId, memory, incomingEvent, onUsage, agentId, agreements);
657
+ }
658
+ if (decision.tool) {
659
+ return await executeTool(decision, eff, sessionId, memory, incomingEvent, onUsage, agentId, agreements);
653
660
  }
654
- if (decision.tool)
655
- return await executeTool(decision, eff, sessionId, memory, incomingEvent, onUsage);
656
661
  if (decision.messages && Array.isArray(decision.messages) && decision.messages.length > 0) {
657
662
  return await executeSendMessages(decision, eff, sessionId);
658
663
  }
@@ -775,7 +780,7 @@ async function executeSendMessages(decision, eff, sessionId) {
775
780
  * dependencies (taskService, agreementService, agents list, etc.), and
776
781
  * wrapping the call in telemetry / operation-record entries if it wants.
777
782
  */
778
- async function executeTool(decision, eff, sessionId, memory, incomingEvent = null, onUsage) {
783
+ async function executeTool(decision, eff, sessionId, memory, incomingEvent = null, onUsage, agentId = null, agreements = null) {
779
784
  const { tool, args } = decision;
780
785
  if (!tool)
781
786
  return null;
@@ -788,13 +793,8 @@ async function executeTool(decision, eff, sessionId, memory, incomingEvent = nul
788
793
  memory,
789
794
  senderId: incomingEvent?.senderId != null ? String(incomingEvent.senderId) : undefined,
790
795
  senderType: incomingEvent?.senderType != null ? String(incomingEvent.senderType) : undefined,
791
- // the subcontract agreement this wake is under (task events carry
792
- // it under `result.agreementId`) so a delegated per-user MCP call can
793
- // resolve the originating end-user, not the delegating agent (senderId).
794
- onBehalfOfAgreementId: (() => {
795
- const aid = incomingEvent?.result?.agreementId;
796
- return typeof aid === 'string' && aid ? aid : undefined;
797
- })(),
796
+ // Never bind on-behalf to a sub the agent only pays for.
797
+ onBehalfOfAgreementId: resolveOnBehalfAgreementId(incomingEvent ?? undefined, agreements, agentId, sessionId),
798
798
  });
799
799
  if (r.usage)
800
800
  onUsage?.(r.usage);
@@ -94,7 +94,7 @@ export function validateWorkflow(workflow) {
94
94
  errors.push(`thinking state "${name}" requires at least one action`);
95
95
  }
96
96
  if (state.timeoutMs !== undefined) {
97
- errors.push(`state "${name}" is thinking but declares timeoutMs — liveness timers apply to parked states only (thinking turns are bounded by AgentHost tickTimeoutMs)`);
97
+ errors.push(`state "${name}" is thinking but declares timeoutMs — liveness timers apply to parked states only (thinking turns are bounded by Agent tickTimeoutMs)`);
98
98
  }
99
99
  }
100
100
  if (state.kind === 'mechanical') {
@@ -61,9 +61,14 @@ export declare function skillHash(files: SkillFile[]): string;
61
61
  export declare function parseSkill(files: SkillFile[], opts?: {
62
62
  maxChars?: number;
63
63
  }): ParsedSkill;
64
+ /** Catalog name for a reference path (`references/tone.md` → `tone`). */
65
+ export declare function skillReferenceName(path: string): string;
66
+ /** First prose line of a reference, for the on-prompt catalog. */
67
+ export declare function skillReferenceOneLine(content: string): string;
64
68
  /**
65
- * Assemble the skill's role prompt: body first, then each reference under its
66
- * own header.
69
+ * Assemble the skill's role prompt: the body, then a catalog of references.
70
+ * Full reference text is fetched with `skill_read_reference` — it does not
71
+ * ride every turn.
67
72
  *
68
73
  * This is only the ROLE block. The Ziggs protocol is prepended separately, by
69
74
  * the one place that builds a Claude-brain system prompt — a skill is never in a