@ziggs-ai/agent-sdk 0.15.0 → 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 (119) hide show
  1. package/CHANGELOG.md +10 -0
  2. package/README.md +110 -88
  3. package/dist/{AgentHost.d.ts → Agent.d.ts} +66 -27
  4. package/dist/{AgentHost.js → Agent.js} +221 -119
  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} +145 -95
  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 +2 -2
  14. package/dist/{agent → brain}/claudeSdkProtocol.js +2 -2
  15. package/dist/{agent → brain}/claudeSdkTools.d.ts +2 -1
  16. package/dist/{agent → brain}/claudeSdkTools.js +9 -65
  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}/agreements/AgreementService.d.ts +10 -1
  40. package/dist/{server → platform}/agreements/AgreementService.js +9 -1
  41. package/dist/{server → platform}/inboxLoop.d.ts +34 -1
  42. package/dist/{server → platform}/inboxLoop.js +122 -14
  43. package/dist/{server → platform}/polls/PollKind.d.ts +4 -7
  44. package/dist/{server → platform}/polls/PollKind.js +3 -6
  45. package/dist/{server → platform}/polls/PollRunner.d.ts +5 -6
  46. package/dist/{server → platform}/polls/PollRunner.js +24 -12
  47. package/dist/platform/polls/declaredPollRunner.d.ts +4 -0
  48. package/dist/platform/polls/declaredPollRunner.js +29 -0
  49. package/dist/{server → platform}/tools/index.d.ts +1 -0
  50. package/dist/{server → platform}/tools/index.js +1 -0
  51. package/dist/{server → platform}/tools/tier1/protocolRunner.js +33 -27
  52. package/dist/{server → platform}/tools/tier1/protocolTools.js +2 -7
  53. package/dist/{server → platform}/tools/tier2/mcpConnectionTools.js +6 -3
  54. package/dist/platform/tools/tier2/taskTools.d.ts +3 -0
  55. package/dist/platform/tools/tier2/taskTools.js +6 -0
  56. package/dist/runtime/bootstrapCtx.d.ts +1 -1
  57. package/dist/runtime/bootstrapCtx.js +1 -1
  58. package/dist/runtime/buildOutcome.d.ts +0 -8
  59. package/dist/runtime/buildOutcome.js +20 -6
  60. package/dist/runtime/kickoff.js +1 -1
  61. package/dist/runtime/runTurn.d.ts +1 -1
  62. package/dist/runtime/runTurn.js +18 -18
  63. package/dist/runtime/validateWorkflow.js +1 -1
  64. package/dist/runtime/wakeOutcome.d.ts +27 -0
  65. package/dist/runtime/wakeOutcome.js +43 -0
  66. package/dist/skills/parseSkill.d.ts +7 -2
  67. package/dist/skills/parseSkill.js +22 -5
  68. package/dist/skills/skillReadReference.d.ts +6 -0
  69. package/dist/skills/skillReadReference.js +27 -0
  70. package/dist/tools/needs.js +2 -2
  71. package/dist/tools/recordReport.d.ts +5 -0
  72. package/dist/tools/recordReport.js +5 -0
  73. package/dist/tools/toolCreds.d.ts +1 -1
  74. package/dist/tools/toolCreds.js +1 -1
  75. package/dist/types.d.ts +3 -3
  76. package/package.json +2 -2
  77. package/dist/server/createHealthServer.d.ts +0 -7
  78. package/dist/server/createHealthServer.js +0 -75
  79. package/dist/server/runLauncher.d.ts +0 -22
  80. package/dist/server/runLauncher.js +0 -160
  81. /package/dist/{agent → brain}/toolManagerToSdkMcp.d.ts +0 -0
  82. /package/dist/{server → platform}/EventQueue.js +0 -0
  83. /package/dist/{server → platform}/OutboxBuffer.d.ts +0 -0
  84. /package/dist/{server → platform}/OutboxBuffer.js +0 -0
  85. /package/dist/{server → platform}/SeenMessages.d.ts +0 -0
  86. /package/dist/{server → platform}/SeenMessages.js +0 -0
  87. /package/dist/{server → platform}/proactive/ProactiveTrigger.d.ts +0 -0
  88. /package/dist/{server → platform}/proactive/ProactiveTrigger.js +0 -0
  89. /package/dist/{server → platform}/requestMatch.d.ts +0 -0
  90. /package/dist/{server → platform}/requestMatch.js +0 -0
  91. /package/dist/{server → platform}/tasks/TaskService.d.ts +0 -0
  92. /package/dist/{server → platform}/tasks/TaskService.js +0 -0
  93. /package/dist/{server → platform}/tasks/index.d.ts +0 -0
  94. /package/dist/{server → platform}/tasks/index.js +0 -0
  95. /package/dist/{server → platform}/telemetryIngest.d.ts +0 -0
  96. /package/dist/{server → platform}/telemetryIngest.js +0 -0
  97. /package/dist/{server → platform}/tools/tier1/protocolRunner.d.ts +0 -0
  98. /package/dist/{server → platform}/tools/tier1/protocolTools.d.ts +0 -0
  99. /package/dist/{server → platform}/tools/tier2/artifactTools.d.ts +0 -0
  100. /package/dist/{server → platform}/tools/tier2/artifactTools.js +0 -0
  101. /package/dist/{server → platform}/tools/tier2/chatTools.d.ts +0 -0
  102. /package/dist/{server → platform}/tools/tier2/chatTools.js +0 -0
  103. /package/dist/{server → platform}/tools/tier2/connectionTools.d.ts +0 -0
  104. /package/dist/{server → platform}/tools/tier2/connectionTools.js +0 -0
  105. /package/dist/{server → platform}/tools/tier2/contextTools.d.ts +0 -0
  106. /package/dist/{server → platform}/tools/tier2/contextTools.js +0 -0
  107. /package/dist/{server → platform}/tools/tier2/discoveryTools.d.ts +0 -0
  108. /package/dist/{server → platform}/tools/tier2/discoveryTools.js +0 -0
  109. /package/dist/{server → platform}/tools/tier2/grantsTools.d.ts +0 -0
  110. /package/dist/{server → platform}/tools/tier2/grantsTools.js +0 -0
  111. /package/dist/{server → platform}/tools/tier2/introductionTools.d.ts +0 -0
  112. /package/dist/{server → platform}/tools/tier2/introductionTools.js +0 -0
  113. /package/dist/{server → platform}/tools/tier2/linkTools.d.ts +0 -0
  114. /package/dist/{server → platform}/tools/tier2/linkTools.js +0 -0
  115. /package/dist/{server → platform}/tools/tier2/marketplaceTools.d.ts +0 -0
  116. /package/dist/{server → platform}/tools/tier2/marketplaceTools.js +0 -0
  117. /package/dist/{server → platform}/tools/tier2/mcpConnectionTools.d.ts +0 -0
  118. /package/dist/{server → platform}/tools/tier2/paymentTools.d.ts +0 -0
  119. /package/dist/{server → platform}/tools/tier2/paymentTools.js +0 -0
@@ -1,30 +1,34 @@
1
+ import { agreementLaneId, agreementIdFromLane } from '@ziggs-ai/api-client';
2
+ import { createDeclaredPollRunner } from './platform/polls/declaredPollRunner.js';
1
3
  import { randomUUID } from 'node:crypto';
2
- import { Agent } from './agent/Agent.js';
3
- import { ClaudeSdkAgent } from './agent/ClaudeSdkAgent.js';
4
- import { applyBrainToHostOptions, isClaudeCognition, } from './brain/index.js';
5
- import { CLAUDE_SDK_TOOLS } from './agent/claudeSdkTools.js';
6
- import { resolveClaudeModel } from './agent/claudeCognition.js';
7
- import { CHAT_TOOLS } from './server/tools/tier2/chatTools.js';
8
- import { EventQueue } from './server/EventQueue.js';
9
- import { StateTimers } from './server/StateTimers.js';
10
- import { createZiggsEffectHandler } from './server/ZiggsEffectHandler.js';
4
+ import { ZiggsBrain } from './brain/ZiggsBrain.js';
5
+ import { ClaudeBrain } from './brain/ClaudeBrain.js';
6
+ import { applyBrainToAgentOptions, isClaudeCognition, } from './brain/index.js';
7
+ import { CLAUDE_SDK_TOOLS } from './brain/claudeSdkTools.js';
8
+ import { resolveClaudeModel } from './brain/claudeCognition.js';
9
+ import { CHAT_TOOLS } from './platform/tools/tier2/chatTools.js';
10
+ import { EventQueue } from './platform/EventQueue.js';
11
+ import { StateTimers } from './platform/StateTimers.js';
12
+ import { createZiggsEffectHandler } from './platform/ZiggsEffectHandler.js';
11
13
  import { normalizeIncomingEvent } from './ingress/normalizeIncoming.js';
12
14
  import { PromptBuilder } from './runtime/PromptBuilder.js';
13
- import { InMemoryStore } from './memory/MemoryStore.js';
15
+ import { InMemoryStore, memorySectionForWake, partitionMemoryStore, } from './memory/MemoryStore.js';
14
16
  import { ToolManager } from './tools/ToolManager.js';
15
17
  import { toolName } from './tools/defineTool.js';
16
- import { PROTOCOL_TOOLS } from './server/tools/tier1/protocolTools.js';
17
- import { marketplaceViewTool } from './server/tools/tier2/marketplaceTools.js';
18
+ import { PROTOCOL_TOOLS } from './platform/tools/tier1/protocolTools.js';
19
+ import { marketplaceViewTool } from './platform/tools/tier2/marketplaceTools.js';
18
20
  import { OpenAIAdapter } from './adapters/OpenAIAdapter.js';
19
21
  import { AnthropicAdapter } from './adapters/AnthropicAdapter.js';
20
22
  import { ArtifactsClient, ConnectionsClient, ContextReadClient, TelemetryClient, sendChatMessage, } from '@ziggs-ai/api-client';
21
23
  import { normalizeNeeds, unmetNeeds, unmetNeedsMessage, } from './tools/needs.js';
22
- import { TaskService } from './server/tasks/TaskService.js';
23
- import { AgreementService } from './server/agreements/AgreementService.js';
24
- import { runInboxLoop } from './server/inboxLoop.js';
25
- import { requestExactMatch } from './server/requestMatch.js';
26
- import { SeenMessages } from './server/SeenMessages.js';
24
+ import { TaskService } from './platform/tasks/TaskService.js';
25
+ import { AgreementService } from './platform/agreements/AgreementService.js';
26
+ import { runInboxLoop } from './platform/inboxLoop.js';
27
+ import { requestExactMatch } from './platform/requestMatch.js';
28
+ import { SeenMessages } from './platform/SeenMessages.js';
27
29
  import { runtimeLog } from './shared/runtimeLog.js';
30
+ import { isTelemetryIngestEnabled, sendTelemetry, } from './platform/telemetryIngest.js';
31
+ import { buildWakeOutcomePayload, classifyWakeOutcome, } from './runtime/wakeOutcome.js';
28
32
  /**
29
33
  * The single fleet-wide default model — the one place the fallback lives.
30
34
  * Change this (or set `LLM_MODEL`) to move the whole fleet; nothing hardcodes a
@@ -34,9 +38,9 @@ import { runtimeLog } from './shared/runtimeLog.js';
34
38
  */
35
39
  export const DEFAULT_MODEL = 'claude-haiku-4-5';
36
40
  // ConnectionPool lives in a separate module; resolved via a computed-path
37
- // dynamic import to avoid a circular import with AgentHost.
41
+ // dynamic import to avoid a circular import with Agent.
38
42
  async function loadConnectionPool() {
39
- const modPath = './server/ConnectionPool.js';
43
+ const modPath = './platform/ConnectionPool.js';
40
44
  const mod = await import(/* @vite-ignore */ modPath);
41
45
  return mod.ConnectionPool;
42
46
  }
@@ -55,10 +59,30 @@ export function laneSessionIdForTask(task) {
55
59
  return task.originChatId;
56
60
  }
57
61
  if (typeof task.agreementId === 'string' && task.agreementId) {
58
- return `agrn-${task.agreementId}`;
62
+ return agreementLaneId(task.agreementId);
59
63
  }
60
64
  return 'default';
61
65
  }
66
+ /** Pull the cost-record keys off a wake event or session id. */
67
+ export function workScopeFromWake(event, sessionId) {
68
+ const rec = event && typeof event === 'object' ? event : {};
69
+ const resource = rec.resource && typeof rec.resource === 'object'
70
+ ? rec.resource
71
+ : {};
72
+ const agreementId = (typeof rec.agreementId === 'string' && rec.agreementId) ||
73
+ (typeof resource.agreementId === 'string' && resource.agreementId) ||
74
+ agreementIdFromLane(rec.chatId) ||
75
+ agreementIdFromLane(resource.chatId) ||
76
+ agreementIdFromLane(sessionId) ||
77
+ undefined;
78
+ const taskId = (typeof rec.taskId === 'string' && rec.taskId) ||
79
+ (typeof resource.taskId === 'string' && resource.taskId) ||
80
+ undefined;
81
+ return {
82
+ ...(agreementId ? { agreementId } : {}),
83
+ ...(taskId ? { taskId } : {}),
84
+ };
85
+ }
62
86
  function createToolManager(options) {
63
87
  const toolManager = new ToolManager();
64
88
  const userTools = options.tools || options.services?.tools || [];
@@ -105,7 +129,7 @@ export function warnOnShadowedPlatformTools(platformTools, ownTools, agentId) {
105
129
  .filter((name) => platform.has(name));
106
130
  if (shadowed.length === 0)
107
131
  return;
108
- console.warn(`[AgentHost] ${agentId}: tool name(s) ${shadowed.join(', ')} shadow the platform tools of the same name — ` +
132
+ console.warn(`[Agent] ${agentId}: tool name(s) ${shadowed.join(', ')} shadow the platform tools of the same name — ` +
109
133
  'the agent will call yours, not Ziggs\'s. Rename them unless that is deliberate.');
110
134
  }
111
135
  function isLlmAdapter(v) {
@@ -151,19 +175,18 @@ function createBackendServices(options) {
151
175
  };
152
176
  }
153
177
  /**
154
- * AgentHost — the Server. Owns a pure {@link Agent}, an {@link EffectHandler},
155
- * a transport (Ziggs WebSocket), and the lane router (per-session FIFO queue
156
- * + in-memory session state).
157
- *
158
- * transport (WS) → handleMessage → agent.tick(.. , eff) → ..
178
+ * One agent identity and its body: inbox, tools, contracts, memory and lanes.
179
+ * The brain decides on each wake; platform services perform its effects.
180
+ * connectAsync starts outbound inbox reads and declared external polls.
181
+ * An Agent never opens a listening socket.
159
182
  */
160
- export class AgentHost {
183
+ export class Agent {
161
184
  options;
162
185
  toolManager;
163
186
  // Retained for external inspection. Undefined for the Claude brain, which
164
187
  // drives query() directly and needs no LLM adapter.
165
188
  llm;
166
- agent;
189
+ brain;
167
190
  eff;
168
191
  eventQueue;
169
192
  sessions = new Map();
@@ -173,12 +196,15 @@ export class AgentHost {
173
196
  sessionStore;
174
197
  taskService;
175
198
  agreementService;
199
+ telemetryClient;
176
200
  messageSender;
177
201
  seenMessages = new SeenMessages();
178
202
  stateTimers;
179
203
  /** The per-agent inbox long-poll loop — this agent's sole wake source. */
180
204
  inboxLoop = null;
181
205
  inboxLoopEnabled;
206
+ started = false;
207
+ pollRunner;
182
208
  /** Short-lived grant-provider cache for the declared-needs gate. */
183
209
  /**
184
210
  * Lanes where an unmet-need refusal has already been allowed its one turn to
@@ -190,7 +216,7 @@ export class AgentHost {
190
216
  unmetTasksPendingAsk = new Map();
191
217
  grantProviderCache = null;
192
218
  constructor(options = {}) {
193
- const resolved = applyBrainToHostOptions(options);
219
+ const resolved = applyBrainToAgentOptions(options);
194
220
  this.options = {
195
221
  openaiKey: resolved.openaiKey || process.env.OPENAI_API_KEY,
196
222
  anthropicKey: resolved.anthropicKey || process.env.ANTHROPIC_API_KEY,
@@ -223,6 +249,8 @@ export class AgentHost {
223
249
  inboxCatchUp: resolved.inboxCatchUp,
224
250
  telemetryIngest: resolved.telemetryIngest,
225
251
  brain: resolved.brain,
252
+ polls: resolved.polls,
253
+ pollKinds: resolved.pollKinds,
226
254
  cognition: resolved.cognition ?? 'fsm',
227
255
  claudeMaxTurns: resolved.claudeMaxTurns,
228
256
  claudeStatelessPerWake: resolved.claudeStatelessPerWake,
@@ -235,12 +263,15 @@ export class AgentHost {
235
263
  // default.
236
264
  this.tickTimeoutMs =
237
265
  options.tickTimeoutMs ??
238
- (isClaudeCognition(options.cognition)
266
+ (isClaudeCognition(resolved.cognition)
239
267
  ? Number(process.env.CLAUDE_SDK_TICK_TIMEOUT_MS ?? 300_000)
240
268
  : 120_000);
241
269
  this.memory = options.memory ?? new InMemoryStore();
242
270
  this.sessionStore = options.sessionStore ?? null;
243
271
  this._validate();
272
+ this.pollRunner = createDeclaredPollRunner(this.options, {
273
+ sendTo: (_agentId, text, metadata) => this.handleMessage(text, metadata ?? {}),
274
+ });
244
275
  this._setup();
245
276
  }
246
277
  _validate() {
@@ -290,6 +321,7 @@ export class AgentHost {
290
321
  }, creds);
291
322
  this.taskService = services.taskService;
292
323
  this.agreementService = services.agreementService;
324
+ this.telemetryClient = services.telemetryClient;
293
325
  this.messageSender = messageSender;
294
326
  let workflow = this.options.workflow;
295
327
  if (!workflow && this.options.states) {
@@ -302,7 +334,7 @@ export class AgentHost {
302
334
  }
303
335
  if (!workflow?.states) {
304
336
  // The Claude brain has no FSM — it drives query() per wake and parks. It
305
- // still plugs into the host's lane/timer machinery via a trivial parked
337
+ // still plugs into the agent's lane/timer machinery via a trivial parked
306
338
  // workflow, synthesized here so callers don't hand-write a dummy one.
307
339
  if (isClaudeCognition(this.options.cognition)) {
308
340
  workflow = {
@@ -313,7 +345,7 @@ export class AgentHost {
313
345
  };
314
346
  }
315
347
  else {
316
- throw new Error('AgentHost: workflow (or states + initial) is required');
348
+ throw new Error('Agent: workflow (or states + initial) is required');
317
349
  }
318
350
  }
319
351
  this.eff = createZiggsEffectHandler({
@@ -330,8 +362,8 @@ export class AgentHost {
330
362
  telemetryIngest: this.options.telemetryIngest,
331
363
  effectTap: this.options.effectTap,
332
364
  });
333
- this.agent = isClaudeCognition(this.options.cognition)
334
- ? new ClaudeSdkAgent({
365
+ this.brain = isClaudeCognition(this.options.cognition)
366
+ ? new ClaudeBrain({
335
367
  workflow,
336
368
  toolManager: services.toolManager,
337
369
  description: this.options.description,
@@ -347,7 +379,7 @@ export class AgentHost {
347
379
  maxTurns: this.options.claudeMaxTurns,
348
380
  statelessPerWake: this.options.claudeStatelessPerWake,
349
381
  })
350
- : new Agent({
382
+ : new ZiggsBrain({
351
383
  workflow,
352
384
  toolManager: services.toolManager,
353
385
  promptBuilder: services.promptBuilder,
@@ -358,7 +390,7 @@ export class AgentHost {
358
390
  // than left as an unhandled rejection (made enqueue reject).
359
391
  this.stateTimers = new StateTimers((ev, laneKey) => {
360
392
  void this.eventQueue.enqueue(ev, laneKey).catch((err) => {
361
- runtimeLog.warn('AgentHost', `state-timer frame failed laneKey=${laneKey}: ${err?.message || err}`);
393
+ runtimeLog.warn('Agent', `state-timer frame failed laneKey=${laneKey}: ${err?.message || err}`);
362
394
  });
363
395
  });
364
396
  }
@@ -400,7 +432,7 @@ export class AgentHost {
400
432
  event.resourceId) ||
401
433
  null;
402
434
  if (agreementId)
403
- sessionId = `agrn-${agreementId}`;
435
+ sessionId = agreementLaneId(agreementId);
404
436
  }
405
437
  if (!sessionId)
406
438
  return;
@@ -427,14 +459,14 @@ export class AgentHost {
427
459
  async handleMessage(text, metadata = {}) {
428
460
  const messageId = metadata.messageId;
429
461
  if (messageId && this.seenMessages.has(messageId)) {
430
- runtimeLog.debug('AgentHost', `skip duplicate messageId=${messageId}`);
462
+ runtimeLog.debug('Agent', `skip duplicate messageId=${messageId}`);
431
463
  return;
432
464
  }
433
465
  if (messageId)
434
466
  this.seenMessages.mark(messageId);
435
467
  const ownAgentId = this._resolveOwnAgentId();
436
468
  if (!ownAgentId) {
437
- runtimeLog.warn('AgentHost', 'handleMessage: ownAgentId missing — dropping event');
469
+ runtimeLog.warn('Agent', 'handleMessage: ownAgentId missing — dropping event');
438
470
  return;
439
471
  }
440
472
  let enrichedText = typeof text === 'string' ? text : '';
@@ -454,14 +486,14 @@ export class AgentHost {
454
486
  const normalized = normalizeIncomingEvent({ text: enrichedText, metadata, ownAgentId });
455
487
  const sessionId = this.sessionResolver(normalized, ownAgentId);
456
488
  if (!sessionId) {
457
- runtimeLog.warn('AgentHost', `session resolver returned null ownAgentId=${ownAgentId} chatId=${normalized.chatId ?? 'null'}`);
489
+ runtimeLog.warn('Agent', `session resolver returned null ownAgentId=${ownAgentId} chatId=${normalized.chatId ?? 'null'}`);
458
490
  return;
459
491
  }
460
492
  const laneKey = `${ownAgentId}:${sessionId}`;
461
493
  if (!this.sessions.has(laneKey)) {
462
494
  this.sessions.set(laneKey, { sessionId, ownAgentId });
463
495
  }
464
- // The inbox loop owns ack (act-before-ack, delivery law); the host only
496
+ // The inbox loop owns ack (act-before-ack, delivery law); the agent only
465
497
  // routes the event onto its lane. No live-path watermark ack — there is no
466
498
  // separate live/push channel to reconcile against anymore.
467
499
  //
@@ -494,10 +526,55 @@ export class AgentHost {
494
526
  * Failing open is deliberate. If the lookup errors we run the turn — a missed
495
527
  * suppression costs tokens, while a wrongly suppressed turn drops work.
496
528
  */
529
+ recordWakeOutcome(session, outcome, llmCalls = 0) {
530
+ const agentId = session.ownAgentId || this.options.agentId;
531
+ const laneKey = `${agentId}:${session.sessionId}`;
532
+ runtimeLog.info(this.options.name || 'agent', `wake-outcome agent=${agentId} lane=${laneKey} kind=${outcome.kind}` +
533
+ (outcome.reason ? ` reason=${outcome.reason}` : ''));
534
+ sendTelemetry(this.telemetryClient, buildWakeOutcomePayload({
535
+ agentId,
536
+ sessionId: session.sessionId,
537
+ laneKey,
538
+ outcome,
539
+ llmCalls,
540
+ }), isTelemetryIngestEnabled(this.options.telemetryIngest));
541
+ }
542
+ memoryForWake(session) {
543
+ return partitionMemoryStore(this.memory, memorySectionForWake({
544
+ sessionId: session.sessionId,
545
+ activeAgreementId: session.ctx
546
+ ?.activeAgreementId,
547
+ }));
548
+ }
549
+ /**
550
+ * A chat-lane session that still carries a cancelled hire from a previous
551
+ * engagement will agreement_get that dead row and stop. Drop it so this
552
+ * wake keys on the live room / hire.
553
+ */
554
+ async dropStaleCancelledAgreementSession(session) {
555
+ const ctx = session.ctx;
556
+ const pinned = ctx?.activeAgreementId;
557
+ if (!pinned || !this.agreementService)
558
+ return;
559
+ if (session.sessionId === agreementLaneId(pinned))
560
+ return;
561
+ try {
562
+ const agreement = await this.agreementService.get(pinned);
563
+ const status = String(agreement?.status ?? '');
564
+ if (status === 'cancelled' ||
565
+ status === 'refunded' ||
566
+ status === 'fulfilled') {
567
+ runtimeLog.info(this.options.name || 'agent', `dropping session pinned to terminal agreement ${pinned} lane=${session.sessionId}`);
568
+ ctx.activeAgreementId = null;
569
+ ctx.claudeSessionId = undefined;
570
+ }
571
+ }
572
+ catch {
573
+ // Fail open: a lookup error must not drop a live hire's session.
574
+ }
575
+ }
497
576
  async laneAgreementIsTerminal(sessionId) {
498
- if (!sessionId.startsWith('agrn-'))
499
- return false;
500
- const agreementId = sessionId.slice('agrn-'.length);
577
+ const agreementId = agreementIdFromLane(sessionId);
501
578
  if (!agreementId || !this.agreementService)
502
579
  return false;
503
580
  try {
@@ -693,10 +770,12 @@ export class AgentHost {
693
770
  return;
694
771
  if (await this.laneAgreementIsTerminal(session.sessionId)) {
695
772
  runtimeLog.info(this.options.name || 'agent', `skipping wake on ${session.sessionId} — its agreement is terminal`);
773
+ this.recordWakeOutcome(session, { kind: 'no-op', reason: 'agreement_terminal' });
696
774
  return;
697
775
  }
698
776
  if (this.isSettledTaskEcho(event)) {
699
777
  runtimeLog.info(this.options.name || 'agent', `skipping wake on ${session.sessionId} — its task already reported a result`);
778
+ this.recordWakeOutcome(session, { kind: 'no-op', reason: 'task_already_settled' });
700
779
  return;
701
780
  }
702
781
  // Before the tick spends anything: work this agent declared it cannot do
@@ -722,14 +801,16 @@ export class AgentHost {
722
801
  }
723
802
  }
724
803
  catch (err) {
725
- runtimeLog.warn('AgentHost', `sessionStore.load failed laneKey=${laneKey}: ${err?.message || err}`);
804
+ runtimeLog.warn('Agent', `sessionStore.load failed laneKey=${laneKey}: ${err?.message || err}`);
726
805
  }
727
806
  }
807
+ await this.dropStaleCancelledAgreementSession(session);
728
808
  const identity = {
729
809
  agentId: session.ownAgentId || this.options.agentId,
730
810
  sessionId: session.sessionId,
731
811
  laneKey,
732
812
  };
813
+ const memory = this.memoryForWake(session);
733
814
  // Recorded here rather than after the tick: this is the last point where the
734
815
  // input is still exactly what the brain will see, and a wake that goes on to
735
816
  // throw is the one most worth replaying.
@@ -745,7 +826,7 @@ export class AgentHost {
745
826
  });
746
827
  }
747
828
  catch (err) {
748
- runtimeLog.warn('AgentHost', `wakeTap failed: ${err?.message || err}`);
829
+ runtimeLog.warn('Agent', `wakeTap failed: ${err?.message || err}`);
749
830
  }
750
831
  }
751
832
  // The watchdog timer must be cleared once the tick settles — leaving it
@@ -757,9 +838,20 @@ export class AgentHost {
757
838
  // a zombie — burning tokens and racing the redelivered retry of the same
758
839
  // event (duplicate sends, runaway per-wake input).
759
840
  const tickAbort = new AbortController();
841
+ const marks = { sent: 0, tasks: 0 };
842
+ const eff = async (effect) => {
843
+ if (effect.kind === 'send-message')
844
+ marks.sent += 1;
845
+ if (effect.kind === 'tool-call') {
846
+ const name = 'name' in effect ? String(effect.name) : '';
847
+ if (name.startsWith('task_'))
848
+ marks.tasks += 1;
849
+ }
850
+ return this.eff(effect);
851
+ };
760
852
  try {
761
853
  const result = await Promise.race([
762
- this.agent.tick({ event: frame, ctx: session.ctx, state: session.state, identity, eff: this.eff, memory: this.memory, transitionTap: this.options.transitionTap, signal: tickAbort.signal }),
854
+ this.brain.tick({ event: frame, ctx: session.ctx, state: session.state, identity, eff, memory, transitionTap: this.options.transitionTap, signal: tickAbort.signal }),
763
855
  new Promise((_, reject) => {
764
856
  tickWatchdog = setTimeout(() => {
765
857
  tickAbort.abort(new Error('tick timeout'));
@@ -770,6 +862,13 @@ export class AgentHost {
770
862
  session.ctx = result.ctx;
771
863
  session.state = result.state;
772
864
  this.sessions.set(laneKey, session);
865
+ this.recordWakeOutcome(session, classifyWakeOutcome({
866
+ lastOutcomeKind: result.ctx
867
+ ?.lastOutcome?.kind,
868
+ llmCalls: result.metrics?.llmCalls ?? 0,
869
+ sentMessage: marks.sent > 0,
870
+ taskWrite: marks.tasks > 0,
871
+ }), result.metrics?.llmCalls ?? 0);
773
872
  // One line per wake that actually spent LLM budget — the handle for
774
873
  // tracking a hosted agent live (grep `agent=<id>`). Parked no-op ticks
775
874
  // (FSM exits, wake-rate-skipped Claude wakes) report zero tokens and
@@ -777,7 +876,7 @@ export class AgentHost {
777
876
  // these lines for one lane. Tokens are billable input (incl. cache).
778
877
  const m = result.metrics;
779
878
  if (m && (m.tokens?.total ?? 0) > 0) {
780
- runtimeLog.info('AgentHost', `wake agent=${identity.agentId} lane=${laneKey} llmCalls=${m.llmCalls} ` +
879
+ runtimeLog.info('Agent', `wake agent=${identity.agentId} lane=${laneKey} llmCalls=${m.llmCalls} ` +
781
880
  `tokens_in=${m.tokens.input} tokens_out=${m.tokens.output} total=${m.tokens.total}`);
782
881
  }
783
882
  // Single source of truth for usage/cost: emit the tick's
@@ -791,15 +890,16 @@ export class AgentHost {
791
890
  model: this.options.model ?? 'unknown',
792
891
  cognition: this.options.cognition ?? 'fsm',
793
892
  metrics: result.metrics,
893
+ ...workScopeFromWake(event, session.sessionId),
794
894
  });
795
895
  }
796
896
  catch (err) {
797
- runtimeLog.warn('AgentHost', `metricsTap failed: ${err?.message || err}`);
897
+ runtimeLog.warn('Agent', `metricsTap failed: ${err?.message || err}`);
798
898
  }
799
899
  }
800
900
  if (this.sessionStore) {
801
901
  this.sessionStore.save(laneKey, { ctx: session.ctx, state: session.state }).catch((err) => {
802
- runtimeLog.warn('AgentHost', `sessionStore.save failed laneKey=${laneKey}: ${err?.message || err}`);
902
+ runtimeLog.warn('Agent', `sessionStore.save failed laneKey=${laneKey}: ${err?.message || err}`);
803
903
  });
804
904
  }
805
905
  // Arm only when the timeout was handled: an unhandled state-timeout must
@@ -824,12 +924,16 @@ export class AgentHost {
824
924
  }
825
925
  }
826
926
  catch (err) {
827
- runtimeLog.warn('AgentHost', `tick failed laneKey=${laneKey}: ${err?.message || err}`);
927
+ this.recordWakeOutcome(session, {
928
+ kind: 'no-op',
929
+ reason: 'tick_failed',
930
+ });
931
+ runtimeLog.warn('Agent', `tick failed laneKey=${laneKey}: ${err?.message || err}`);
828
932
  // Deliberate re-arm on failure. Exact scenario: the tick timed out at
829
933
  // tickTimeoutMs (or sessionStore.load threw) leaving session.state at a
830
934
  // timed parked state — without re-arming, that lane would never wake
831
935
  // again unless a real event happens to arrive.
832
- this._armIfTimedParked(laneKey, session.state ?? this.agent.workflow.initial);
936
+ this._armIfTimedParked(laneKey, session.state ?? this.brain.workflow.initial);
833
937
  // Best-effort: also notify the user so the conversation isn't silently
834
938
  // stuck. Complements the timer re-arm above — that recovers a
835
939
  // parked lane; this tells a waiting human.
@@ -855,12 +959,12 @@ export class AgentHost {
855
959
  }
856
960
  }
857
961
  /**
858
- * Re-run one recorded wake against THIS host's agent, and nothing else.
962
+ * Re-run one recorded wake against THIS agent, and nothing else.
859
963
  *
860
964
  * The dev loop for a prompt: record a wake off a live run, edit the agent, and
861
965
  * see what it does differently in seconds instead of standing a world back up
862
966
  * for one turn. The brain, tools and effect handler are the real ones, so the
863
- * turn is real — its tool calls hit the backend this host is pointed at, which
967
+ * turn is real — its tool calls hit the backend this agent is pointed at, which
864
968
  * is why it belongs against a disposable world and not a shared one.
865
969
  *
866
970
  * Deliberately NOT routed through `_processEvent`: everything that method does
@@ -876,19 +980,23 @@ export class AgentHost {
876
980
  sessionId: record.sessionId,
877
981
  laneKey: record.laneKey || record.sessionId,
878
982
  };
983
+ const memory = this.memoryForWake({
984
+ sessionId: record.sessionId,
985
+ ctx: record.ctx,
986
+ });
879
987
  // Same watchdog as a live wake, for the same reason: an unaborted Claude
880
988
  // query outlives the promise that abandoned it and keeps spending.
881
989
  const abort = new AbortController();
882
990
  let watchdog;
883
991
  try {
884
992
  const result = await Promise.race([
885
- this.agent.tick({
993
+ this.brain.tick({
886
994
  event: record.event,
887
995
  ctx: record.ctx,
888
996
  state: record.state,
889
997
  identity,
890
998
  eff: this.eff,
891
- memory: this.memory,
999
+ memory,
892
1000
  transitionTap: this.options.transitionTap,
893
1001
  signal: abort.signal,
894
1002
  }),
@@ -907,10 +1015,11 @@ export class AgentHost {
907
1015
  model: this.options.model ?? 'unknown',
908
1016
  cognition: this.options.cognition ?? 'fsm',
909
1017
  metrics: result.metrics,
1018
+ ...workScopeFromWake(record.event, identity.sessionId),
910
1019
  });
911
1020
  }
912
1021
  catch (err) {
913
- runtimeLog.warn('AgentHost', `metricsTap failed: ${err?.message || err}`);
1022
+ runtimeLog.warn('Agent', `metricsTap failed: ${err?.message || err}`);
914
1023
  }
915
1024
  }
916
1025
  return result;
@@ -923,25 +1032,33 @@ export class AgentHost {
923
1032
  _armIfTimedParked(laneKey, stateName) {
924
1033
  if (!stateName)
925
1034
  return;
926
- const sd = this.agent.workflow.states[stateName];
1035
+ const sd = this.brain.workflow.states[stateName];
927
1036
  if (sd?.kind !== 'parked' || sd.timeoutMs === undefined)
928
1037
  return;
929
1038
  this.stateTimers.arm(laneKey, stateName, sd.timeoutMs);
930
1039
  }
931
1040
  _resolveOwnAgentId() {
932
- // The host is constructed for exactly one agent; its own id is the only
1041
+ // The body is constructed for exactly one identity; its own id is the only
933
1042
  // source. (The socket-frame-era metadata fallbacks — self/agent/ziggsAgentId
934
1043
  // — are gone: nothing on the inbox wire ever carried them.)
935
1044
  return this.options.agentId || null;
936
1045
  }
937
1046
  async connectAsync(_timeout) {
938
- // Bootstrap active assigned tasks once (HTTP pull → lane), then start the
939
- // per-agent inbox long-poll loop — the agent's sole wake source. There is
940
- // no socket to connect and no reconnect: a crash is just the loop's next
941
- // iteration (the cursor is server-side). `_timeout` is accepted for
942
- // backward-compatible call sites but no longer gates a handshake.
1047
+ if (this.started)
1048
+ return this;
1049
+ // Start the per-agent inbox long-poll loop — the agent's sole wake source.
1050
+ // There is no socket to connect and no reconnect: a crash is just the
1051
+ // loop's next iteration (the cursor is server-side). `_timeout` is accepted
1052
+ // for backward-compatible call sites but no longer gates a handshake.
1053
+ //
1054
+ // There is no separate catch-up sweep here any more. `tasksAwaitingMe` is
1055
+ // re-derived from the collection on every pass and redelivers an open
1056
+ // assigned task until it closes, so the loop's first pass IS the catch-up —
1057
+ // a restarted agent sees its standing work with nothing extra to run. The
1058
+ // sweep that used to sit here pulled the same list over
1059
+ // `GET /agents/:id/tasks` and reached one gate the inbox does not, which is
1060
+ // how a withheld join node got handed to its assignee before its turn.
943
1061
  if (this.inboxLoopEnabled) {
944
- await this._bootstrapAssignedActiveTasks();
945
1062
  const label = this.options.name || 'agent';
946
1063
  this.inboxLoop = runInboxLoop({
947
1064
  creds: {
@@ -974,10 +1091,12 @@ export class AgentHost {
974
1091
  },
975
1092
  });
976
1093
  }
1094
+ this.pollRunner?.start();
1095
+ this.started = true;
977
1096
  return this;
978
1097
  }
979
1098
  /**
980
- * plain string compare against {@link AgentHostOptions.requestMatchTags}
1099
+ * plain string compare against {@link AgentOptions.requestMatchTags}
981
1100
  * and `specialization`. No match → return (caller acks). Match → normal wake.
982
1101
  */
983
1102
  async triageRequestDelivery(request) {
@@ -997,52 +1116,6 @@ export class AgentHost {
997
1116
  agreementId: request.agreementId,
998
1117
  });
999
1118
  }
1000
- async _bootstrapAssignedActiveTasks() {
1001
- const agentId = this.options.agentId;
1002
- if (!agentId || !this.taskService)
1003
- return;
1004
- let tasks = [];
1005
- try {
1006
- tasks = await this.taskService.getActiveTasksForAgent(agentId);
1007
- }
1008
- catch (err) {
1009
- runtimeLog.warn(this.options.name || 'agent', `active-task bootstrap skipped: ${err?.message ?? err}`);
1010
- return;
1011
- }
1012
- for (const row of tasks) {
1013
- const taskId = row.taskId;
1014
- if (!taskId)
1015
- continue;
1016
- if (row.assigneeId && row.assigneeId !== agentId)
1017
- continue;
1018
- let full;
1019
- try {
1020
- full = (await this.taskService.getTask(taskId));
1021
- }
1022
- catch {
1023
- continue;
1024
- }
1025
- const state = String(full.state ?? '');
1026
- if (state !== 'active' && state !== 'in-progress')
1027
- continue;
1028
- const sessionId = laneSessionIdForTask(full);
1029
- const laneKey = `${agentId}:${sessionId}`;
1030
- if (!this.sessions.has(laneKey)) {
1031
- this.sessions.set(laneKey, { sessionId, ownAgentId: agentId });
1032
- }
1033
- runtimeLog.info(this.options.name || 'agent', `bootstrap task-assigned ${taskId} (state=${state})`);
1034
- await this.eventQueue.enqueue({
1035
- type: 'task_result',
1036
- result: {
1037
- ...full,
1038
- taskId: full.taskId ?? taskId,
1039
- state,
1040
- providerIsYou: true,
1041
- },
1042
- chatId: sessionId,
1043
- }, laneKey);
1044
- }
1045
- }
1046
1119
  /** replay task-assigned for an existing active task (demo recovery). */
1047
1120
  async wakeTaskAssignment(taskId) {
1048
1121
  const agentId = this.options.agentId;
@@ -1070,7 +1143,13 @@ export class AgentHost {
1070
1143
  chatId: sessionId,
1071
1144
  }, laneKey);
1072
1145
  }
1073
- disconnect() { this.stateTimers.clearAll(); void this.inboxLoop?.stop(); this.inboxLoop = null; }
1146
+ disconnect() {
1147
+ this.started = false;
1148
+ this.pollRunner?.stop();
1149
+ this.stateTimers.clearAll();
1150
+ void this.inboxLoop?.stop();
1151
+ this.inboxLoop = null;
1152
+ }
1074
1153
  /**
1075
1154
  * Waits for in-flight ticks to finish. Call before process.exit() on SIGTERM.
1076
1155
  * A timeout is returned, not thrown — the caller names it (lab verdict, fleet
@@ -1078,20 +1157,43 @@ export class AgentHost {
1078
1157
  */
1079
1158
  drain(timeoutMs = 30_000) { return this.eventQueue.waitForIdle(timeoutMs); }
1080
1159
  registerTool(tool) { return this.toolManager.register(tool); }
1081
- isConnected() { return this.inboxLoop?.running ?? false; }
1160
+ isConnected() { return this.started && (!this.inboxLoopEnabled || (this.inboxLoop?.running ?? false)); }
1161
+ /**
1162
+ * Whether this agent is still reading its inbox, and when it last did. One
1163
+ * signal, three readers: the store's online badge, an operator's health
1164
+ * check, and monitoring. `lastPassAt` is the read itself; `lastAttemptAt`
1165
+ * says the loop still turns when the server is the thing that is down.
1166
+ * A `pollingInbox: false` agent (inboxCatchUp disabled, lab and eval paths)
1167
+ * has nothing to be stale about and reports no timestamps.
1168
+ */
1169
+ inboxLiveness() {
1170
+ return {
1171
+ agentId: this.options.agentId,
1172
+ connected: this.isConnected(),
1173
+ pollingInbox: this.inboxLoopEnabled,
1174
+ lastPassAt: this.inboxLoop?.lastPassAt ?? null,
1175
+ lastAttemptAt: this.inboxLoop?.lastAttemptAt ?? null,
1176
+ };
1177
+ }
1082
1178
  }
1083
1179
  /**
1084
- * createAgent(config) → AgentHost
1180
+ * createAgent(config) → Agent
1085
1181
  * createAgentPool([cfg, ...]) → ConnectionPool (lazy fleet, async-loaded)
1086
1182
  */
1087
1183
  export function createAgent(options = {}) {
1088
- return new AgentHost(options);
1184
+ return new Agent(options);
1089
1185
  }
1090
1186
  export async function createAgentPool(configs) {
1091
1187
  const ConnectionPool = await loadConnectionPool();
1092
1188
  const pool = new ConnectionPool();
1093
1189
  pool.register(configs);
1094
- await pool.connectAll();
1095
- return pool;
1190
+ try {
1191
+ await pool.connectAll();
1192
+ return pool;
1193
+ }
1194
+ catch (err) {
1195
+ await pool.shutdown();
1196
+ throw err;
1197
+ }
1096
1198
  }
1097
- export default AgentHost;
1199
+ export default Agent;