@ziggs-ai/agent-sdk 0.1.4 → 0.1.6

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 (49) hide show
  1. package/README.md +7 -5
  2. package/package.json +2 -2
  3. package/src/AgentHost.ts +165 -12
  4. package/src/adapters/OpenAIAdapter.ts +21 -0
  5. package/src/agent/Agent.ts +5 -2
  6. package/src/cognition/validateContext.ts +1 -1
  7. package/src/context/batch.ts +3 -3
  8. package/src/context/classifyEnvelope.ts +2 -2
  9. package/src/context/routingLabels.ts +1 -1
  10. package/src/formatters/AgreementFormatter.ts +5 -5
  11. package/src/formatters/HistoryFormatter.ts +3 -3
  12. package/src/index.ts +25 -4
  13. package/src/ingress/normalizeIncoming.ts +50 -7
  14. package/src/pricing/fleetDefaults.ts +218 -0
  15. package/src/pricing/fleetEvalFree.ts +24 -0
  16. package/src/pricing/fleetFreeTierA.gen.ts +12 -0
  17. package/src/pricing/fleetTierByAgentId.gen.ts +1022 -0
  18. package/src/runtime/AgentMachine.ts +68 -2
  19. package/src/runtime/PromptBuilder.ts +25 -23
  20. package/src/runtime/buildOutcome.ts +33 -3
  21. package/src/runtime/defaults.ts +3 -0
  22. package/src/runtime/runTurn.ts +115 -61
  23. package/src/runtime/validateWorkflow.ts +16 -0
  24. package/src/server/EventQueue.ts +14 -0
  25. package/src/server/InboxCatchUp.ts +251 -0
  26. package/src/server/SeenMessages.ts +27 -0
  27. package/src/server/ZiggsEffectHandler.ts +82 -8
  28. package/src/server/agreements/AgreementService.ts +7 -1
  29. package/src/server/createHealthServer.ts +79 -2
  30. package/src/server/runLauncher.ts +40 -25
  31. package/src/server/tasks/TaskService.ts +4 -5
  32. package/src/server/tasks/index.ts +0 -3
  33. package/src/server/telemetryIngest.ts +91 -0
  34. package/src/server/tools/index.ts +46 -0
  35. package/src/server/{tasks → tools/tier1}/protocolRunner.ts +52 -20
  36. package/src/server/{tasks → tools/tier1}/protocolTools.ts +6 -3
  37. package/src/server/tools/tier2/connectionTools.ts +75 -0
  38. package/src/server/tools/tier2/contextTools.ts +74 -0
  39. package/src/server/tools/tier2/discoveryTools.ts +34 -0
  40. package/src/server/tools/tier2/marketplaceTools.ts +25 -0
  41. package/src/server/{tasks → tools/tier2}/paymentTools.ts +74 -37
  42. package/src/server/ziggsconnect/ZiggsConnectClient.ts +126 -0
  43. package/src/server/ziggscontext/ZiggsContextClient.ts +137 -0
  44. package/src/server/ziggspay/ZiggsPayClient.ts +12 -12
  45. package/src/shared/types.ts +0 -2
  46. package/src/tools/index.ts +2 -0
  47. package/src/tools/recordReport.ts +82 -0
  48. package/src/types.ts +47 -8
  49. package/src/tasks/taskCore.ts +0 -139
package/README.md CHANGED
@@ -13,7 +13,7 @@ It is **not** a generic chat wrapper: the core idea is a **lightweight state mac
13
13
  | **`defineAgent` / workflow** | Declarative agent: `initial`, `states`, optional `id`, `description`, `specialization`, merged `tools` / `services`. |
14
14
  | **`AgentMachine`** | Interprets the workflow: parked states wait for events; thinking states run `runTurn`; `transitions` picks the next state from context. |
15
15
  | **`runTurn`** | Builds prompts, calls the LLM (with tools), parses the model output, and returns effects for the machine. |
16
- | **`ZiggsAgent`** | Batteries-included process: OpenAI adapter, tool manager, task service, context read/write, **WebSocket** to Ziggs, and an **`Agent`** that dispatches incoming messages into the machine. |
16
+ | **`AgentHost`** | Batteries-included process: OpenAI adapter, tool manager, task service, context clients, **WebSocket** to Ziggs, and an **`Agent`** that dispatches incoming messages into the machine. |
17
17
  | **`Agent`** | Orchestrates message handling, machine lifecycle, and integration with platform APIs (without requiring you to use WebSockets if you construct it yourself). |
18
18
 
19
19
  There is **no** separate compile step: the workflow object is used **directly** by the runtime.
@@ -50,7 +50,7 @@ The `wait` action is built-in: `defineAgent` automatically injects it into every
50
50
  ## How you usually run an agent
51
51
 
52
52
  1. **`defineAgent({ ... })`** → options object including `workflow`.
53
- 2. Pass **`openaiKey`**, **`operatorKey`** (Ziggs operator token), **`agentId`**, **`wsUrl`**, etc., and **`new ZiggsAgent(config)`** (or **`createAgent(config)`**).
53
+ 2. Pass **`openaiKey`**, **`operatorKey`** (Ziggs operator token), **`agentId`**, **`wsUrl`**, etc., and **`createAgent(config)`** (returns an **`AgentHost`**).
54
54
  3. **`connect()`** to open the WebSocket; the SDK routes platform messages into **`handleMessage`**.
55
55
 
56
56
  Examples in the repo: `examples/agents/*.js` (e.g. coffee, expense, delivery agents).
@@ -59,12 +59,14 @@ Examples in the repo: `examples/agents/*.js` (e.g. coffee, expense, delivery age
59
59
 
60
60
  ## Main exports (entry: `src/index.js`)
61
61
 
62
- - **`ZiggsAgent`**, **`createAgent`**, **`defineAgent`**
62
+ - **`AgentHost`**, **`createAgent`**, **`createAgentPool`**, **`defineAgent`**
63
63
  - **`Agent`**, **`AgentMachine`**, **`runTurn`**
64
64
  - **Prompt / tools:** `PromptBuilder`, `ToolManager`, `defineTool`
65
- - **Protocol tools:** `agreementProposeTool`, `agreementDelegateTool`, `agreementRespondTool`, `agreementCounterProposalTool`, `taskSpawnTool`, `taskUpdateTool`, `taskUpdatePlanStepTool`, `PROTOCOL_TOOLS`
65
+ - **Tools come in two tiers single home: `server/tools/`:**
66
+ - **Tier 1 — protocol grammar (`server/tools/tier1/`, `PROTOCOL_TOOLS`).** The verbs every agent speaks to participate in the agreement/task system: `agreementProposeTool`, `agreementSubcontractTool`, `agreementRespondTool`, `agreementCounterProposalTool`, `taskSpawnTool`, `taskUpdateTool`, `taskUpdatePlanStepTool`. **Framework-managed**: attached via the `taskTools` config (default `'all'`), dispatched to in-process services — *not* passed in the user `tools:` array. Publishing open work is part of this grammar: `agreement_propose({ proposedTo: "everyone" })` (there is no separate publish tool).
67
+ - **Tier 2 — capability bundles (`server/tools/tier2/`, opt-in, HTTP-backed).** Discrete capability domains an agent chooses to have. **Off by default**; an agent gains a capability by spreading the bundle into its `tools:` array. Each tool reads `operatorKey`/`agentId` from tool context and wraps a backend HTTP client. Bundles: `DISCOVERY_TOOLS` (`agentSearchTool`, `agentGetTool`), `MARKETPLACE_TOOLS` (`marketplaceViewTool` — read-only), `PAYMENT_TOOLS` (`paymentBalanceTool`, `paymentTransferTool`, …).
66
68
  - **Adapters / utils:** `OpenAIAdapter`, JSON helpers, formatters
67
- - **Re-exported** from `@ziggs-ai/api-client`: `WebSocketClient`, `ContextReader`, `ContextWriter`, URL helpers, etc.
69
+ - **Re-exported** from `@ziggs-ai/api-client`: `WebSocketClient`, `ContextReadClient`, `ContextDiscoveryClient`, `ContextGrantsClient`, `ArtifactsClient`, `MessagesClient`, `ScopeClient`, `AgentSearchClient`, `getBackendUrl`, `getWebSocketUrl`, etc. (There is no `ContextReader` / `ContextWriter` — use the clients above from `@ziggs-ai/api-client` directly.)
68
70
 
69
71
  Package export: `"."` → `src/index.js` (see `package.json`).
70
72
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ziggs-ai/agent-sdk",
3
- "version": "0.1.4",
3
+ "version": "0.1.6",
4
4
  "description": "Agent framework SDK for building autonomous agents on the Ziggs platform",
5
5
  "type": "module",
6
6
  "main": "src/index.ts",
@@ -16,7 +16,7 @@
16
16
  "test:watch": "node --experimental-test-module-mocks --import tsx/esm --test --watch test/*.test.ts"
17
17
  },
18
18
  "dependencies": {
19
- "@ziggs-ai/api-client": "^0.1.4",
19
+ "@ziggs-ai/api-client": "^0.1.9",
20
20
  "ajv": "^8.17.1",
21
21
  "dotenv": "^17.2.3",
22
22
  "openai": "^4.0.0"
package/src/AgentHost.ts CHANGED
@@ -5,9 +5,21 @@ import { createZiggsEffectHandler } from './server/ZiggsEffectHandler.js';
5
5
  import { normalizeIncomingEvent, type NormalizedEvent } from './ingress/normalizeIncoming.js';
6
6
  import { PromptBuilder } from './runtime/PromptBuilder.js';
7
7
  import { InMemoryStore, type MemoryStore } from './memory/MemoryStore.js';
8
+
9
+ /**
10
+ * Operator-provided FSM session persistence.
11
+ *
12
+ * AgentHost calls load() before each tick when a lane has no in-memory state
13
+ * (e.g. after a container restart) and save() after every successful tick.
14
+ * A null return from load() means "no prior state — start fresh."
15
+ */
16
+ export interface SessionStore {
17
+ load(laneKey: string): Promise<{ ctx?: Ctx; state?: string } | null | undefined>;
18
+ save(laneKey: string, session: { ctx?: Ctx; state?: string }): Promise<void>;
19
+ }
8
20
  import { ToolManager } from './tools/ToolManager.js';
9
21
  import type { ToolDefinition } from './tools/defineTool.js';
10
- import { PROTOCOL_TOOLS } from './server/tasks/protocolTools.js';
22
+ import { PROTOCOL_TOOLS } from './server/tools/tier1/protocolTools.js';
11
23
  import { OpenAIAdapter } from './adapters/OpenAIAdapter.js';
12
24
  import {
13
25
  WebSocketClient,
@@ -19,6 +31,8 @@ import {
19
31
  } from '@ziggs-ai/api-client';
20
32
  import { TaskService } from './server/tasks/TaskService.js';
21
33
  import { AgreementService } from './server/agreements/AgreementService.js';
34
+ import { runInboxCatchUp } from './server/InboxCatchUp.js';
35
+ import { SeenMessages } from './server/SeenMessages.js';
22
36
  import { runtimeLog } from './shared/runtimeLog.js';
23
37
 
24
38
  // ConnectionPool lives in a separate module; resolved via a computed-path
@@ -39,9 +53,13 @@ export type SessionResolver = (normalized: NormalizedEvent, ownAgentId: string)
39
53
 
40
54
  const DEFAULT_SESSION_RESOLVER: SessionResolver = (normalized) => normalized.chatId;
41
55
 
56
+ interface LlmAdapter {
57
+ chatMessages(messages: unknown[], tools?: unknown[], options?: Record<string, unknown>): Promise<unknown>;
58
+ }
59
+
42
60
  interface AgentServices {
43
61
  tools?: ToolDefinition[];
44
- llm?: { model?: string };
62
+ llm?: { model?: string } | LlmAdapter;
45
63
  [key: string]: unknown;
46
64
  }
47
65
 
@@ -70,6 +88,23 @@ export interface AgentHostOptions {
70
88
  * pass `new FileMemoryStore(path)` or your own implementation.
71
89
  */
72
90
  memory?: MemoryStore;
91
+ /**
92
+ * FSM session persistence. Operator-provided. When set, AgentHost loads
93
+ * prior state before each tick (cold-start recovery) and saves after every
94
+ * successful tick. Omit to keep state in-process only (default behaviour).
95
+ */
96
+ sessionStore?: SessionStore;
97
+ /** Observability tap: called after every effect with the effect and its response. */
98
+ effectTap?: (effect: import('./types.js').Effect, response: unknown) => void;
99
+ /** Observability tap: called on every FSM state transition, no-match, depth-guard, or error-escape (ZIG-442). */
100
+ transitionTap?: (event: import('./runtime/AgentMachine.js').TransitionEvent) => void;
101
+ /** Production monitoring ingest (ZIG-360). Default on; set false to disable. */
102
+ telemetryIngest?: boolean;
103
+ /**
104
+ * Pull inbox on every WS connect/reconnect (ZIG-454). Default true.
105
+ * Recovers messages lost when backend in-memory pending queues are dropped.
106
+ */
107
+ inboxCatchUp?: boolean;
73
108
  }
74
109
 
75
110
  function createToolManager(options: AgentHostOptions): ToolManager {
@@ -83,9 +118,15 @@ function createToolManager(options: AgentHostOptions): ToolManager {
83
118
  return toolManager;
84
119
  }
85
120
 
121
+ function isLlmAdapter(v: unknown): v is LlmAdapter {
122
+ return typeof v === 'object' && v !== null && typeof (v as LlmAdapter).chatMessages === 'function';
123
+ }
124
+
86
125
  function createBackendServices(options: AgentHostOptions) {
87
126
  const toolManager = createToolManager(options);
88
- const llm = new OpenAIAdapter({ key: options.openaiKey, model: options.services?.llm?.model || options.model });
127
+ const llm = isLlmAdapter(options.services?.llm)
128
+ ? options.services!.llm as LlmAdapter
129
+ : new OpenAIAdapter({ key: options.openaiKey, model: (options.services?.llm as { model?: string } | undefined)?.model || options.model });
89
130
  const operatorKey = options.operatorKey!;
90
131
  const agentId = options.agentId!;
91
132
  const messagesClient = new MessagesClient(operatorKey, agentId);
@@ -125,7 +166,7 @@ interface LaneSession {
125
166
  export class AgentHost {
126
167
  options: Required<Pick<AgentHostOptions, 'agentId' | 'operatorKey' | 'openaiKey'>> & AgentHostOptions;
127
168
  toolManager!: ToolManager;
128
- llm!: OpenAIAdapter; // retained for external inspection
169
+ llm!: OpenAIAdapter | LlmAdapter; // retained for external inspection
129
170
  agent!: Agent;
130
171
  wsClient!: WebSocketClient;
131
172
  private eff!: EffectHandler;
@@ -134,11 +175,18 @@ export class AgentHost {
134
175
  private sessionResolver: SessionResolver;
135
176
  private tickTimeoutMs: number;
136
177
  private memory: MemoryStore;
178
+ private sessionStore: SessionStore | null;
179
+ private taskService!: TaskService;
180
+ private agreementService!: AgreementService;
181
+ private messageSender!: (message: string, receiverId: string, sessionId: string) => Promise<unknown>;
182
+ private seenMessages = new SeenMessages();
183
+ private catchUpInFlight: Promise<void> | null = null;
184
+ private inboxCatchUpEnabled: boolean;
137
185
 
138
186
  constructor(options: AgentHostOptions = {}) {
139
187
  this.options = {
140
188
  openaiKey: options.openaiKey || process.env.OPENAI_API_KEY,
141
- model: options.model || options.services?.llm?.model || 'gpt-5.4',
189
+ model: options.model || (!isLlmAdapter(options.services?.llm) ? options.services?.llm?.model : undefined) || 'gpt-5.4',
142
190
  operatorKey: options.operatorKey || process.env.ZIGGS_OPERATOR_KEY || undefined,
143
191
  agentId: options.agentId || '',
144
192
  name: options.name || null,
@@ -153,10 +201,15 @@ export class AgentHost {
153
201
  workflow: options.workflow,
154
202
  tickTimeoutMs: options.tickTimeoutMs,
155
203
  sessionResolver: options.sessionResolver,
204
+ effectTap: options.effectTap,
205
+ transitionTap: options.transitionTap,
206
+ inboxCatchUp: options.inboxCatchUp,
156
207
  } as Required<Pick<AgentHostOptions, 'agentId' | 'operatorKey' | 'openaiKey'>> & AgentHostOptions;
208
+ this.inboxCatchUpEnabled = options.inboxCatchUp !== false;
157
209
  this.sessionResolver = options.sessionResolver || DEFAULT_SESSION_RESOLVER;
158
210
  this.tickTimeoutMs = options.tickTimeoutMs ?? 120_000;
159
211
  this.memory = options.memory ?? new InMemoryStore();
212
+ this.sessionStore = options.sessionStore ?? null;
160
213
  this._validate();
161
214
  this._setup();
162
215
  }
@@ -179,10 +232,16 @@ export class AgentHost {
179
232
  label: this.options.name || 'agent',
180
233
  });
181
234
 
182
- const messageSender = (message: string, receiverId: string, sessionId: string): Promise<unknown> => {
183
- this.wsClient.send(sessionId, receiverId, message);
235
+ const messageSender = (message: string, receiverId: string, sessionId: string, messageId?: string): Promise<unknown> => {
236
+ this.wsClient.send(sessionId, receiverId, message, messageId ? { messageId } : undefined);
184
237
  return Promise.resolve(undefined);
185
238
  };
239
+ const chunkSender = (sessionId: string, receiverId: string, text: string, messageId: string): void => {
240
+ this.wsClient.send(sessionId, receiverId, text, { partial: true, messageId });
241
+ };
242
+ this.taskService = services.taskService;
243
+ this.agreementService = services.agreementService;
244
+ this.messageSender = messageSender;
186
245
 
187
246
  let workflow = this.options.workflow;
188
247
  if (!workflow && this.options.states) {
@@ -203,10 +262,13 @@ export class AgentHost {
203
262
  taskService: services.taskService,
204
263
  agreementService: services.agreementService,
205
264
  messageSender,
265
+ chunkSender,
206
266
  toolManager: services.toolManager,
207
267
  operatorKey: this.options.operatorKey!,
208
268
  agentId: this.options.agentId!,
209
269
  telemetryClient: services.telemetryClient,
270
+ telemetryIngest: this.options.telemetryIngest,
271
+ effectTap: this.options.effectTap,
210
272
  });
211
273
 
212
274
  this.agent = new Agent({
@@ -219,6 +281,47 @@ export class AgentHost {
219
281
 
220
282
  this.wsClient.setMessageHandler((text: string, metadata: MessageMetadata) => this.handleMessage(text, metadata as unknown as Record<string, unknown>));
221
283
  this.wsClient.setResourceEventHandler((event) => this.handleResourceEvent(event as unknown as Record<string, unknown>));
284
+
285
+ if (this.inboxCatchUpEnabled) {
286
+ this.wsClient.onConnect(() => {
287
+ void this._ensureCatchUp();
288
+ });
289
+ }
290
+ }
291
+
292
+ /**
293
+ * Coalesce concurrent connect/reconnect bursts into one catch-up pass.
294
+ */
295
+ private _ensureCatchUp(): Promise<void> {
296
+ if (!this.catchUpInFlight) {
297
+ this.catchUpInFlight = this._runInboxCatchUp().finally(() => {
298
+ this.catchUpInFlight = null;
299
+ });
300
+ }
301
+ return this.catchUpInFlight;
302
+ }
303
+
304
+ private async _runInboxCatchUp(): Promise<void> {
305
+ const label = this.options.name || 'agent';
306
+ try {
307
+ await runInboxCatchUp({
308
+ creds: {
309
+ operatorKey: this.options.operatorKey!,
310
+ agentId: this.options.agentId!,
311
+ },
312
+ ownAgentId: this.options.agentId!,
313
+ baseUrl: this.options.wsUrl ? undefined : undefined,
314
+ seen: this.seenMessages,
315
+ label,
316
+ onMessage: (text, metadata) => this.handleMessage(text, metadata),
317
+ onResourceEvent: (event) => this.handleResourceEvent(event),
318
+ });
319
+ } catch (err: unknown) {
320
+ runtimeLog.warn(
321
+ label,
322
+ `inbox catch-up failed: ${(err as Error)?.message ?? err}`,
323
+ );
324
+ }
222
325
  }
223
326
 
224
327
  /**
@@ -259,21 +362,38 @@ export class AgentHost {
259
362
 
260
363
  /** Wire entry. Normalize → resolve session → enqueue on the right lane. */
261
364
  async handleMessage(text: string, metadata: Record<string, unknown> = {}): Promise<void> {
365
+ const messageId = metadata.messageId as string | undefined;
366
+ if (messageId && this.seenMessages.has(messageId)) {
367
+ runtimeLog.debug('AgentHost', `skip duplicate messageId=${messageId}`);
368
+ return;
369
+ }
370
+ if (messageId) this.seenMessages.mark(messageId);
371
+
262
372
  const ownAgentId = this._resolveOwnAgentId(metadata);
263
373
  if (!ownAgentId) {
264
374
  runtimeLog.warn('AgentHost', 'handleMessage: ownAgentId missing — dropping event');
265
375
  return;
266
376
  }
267
377
 
268
- const normalized = normalizeIncomingEvent({ text, metadata, ownAgentId });
378
+ let enrichedText = typeof text === 'string' ? text : '';
379
+ if (!enrichedText.trim()) {
380
+ const agreement = metadata.agreement as Record<string, unknown> | undefined;
381
+ const terms = agreement?.terms as { description?: string } | undefined;
382
+ if (terms?.description) {
383
+ enrichedText = String(terms.description);
384
+ }
385
+ }
386
+
387
+ const normalized = normalizeIncomingEvent({ text: enrichedText, metadata, ownAgentId });
269
388
  if (!normalized.shouldProcess) {
270
- runtimeLog.info('AgentHost', `skip reason=${normalized.reason || 'not_relevant'}`);
389
+ const receiverId = (metadata.receiver as Record<string, unknown> | undefined)?.id ?? metadata.receiverId;
390
+ runtimeLog.warn('AgentHost', `skip reason=${normalized.reason || 'not_relevant'} ownAgentId=${ownAgentId} receiverId=${receiverId ?? 'null'} chatId=${normalized.chatId ?? 'null'}`);
271
391
  return;
272
392
  }
273
393
 
274
394
  const sessionId = this.sessionResolver(normalized, ownAgentId);
275
395
  if (!sessionId) {
276
- runtimeLog.warn('AgentHost', 'session resolver returned null, skipping');
396
+ runtimeLog.warn('AgentHost', `session resolver returned null ownAgentId=${ownAgentId} chatId=${normalized.chatId ?? 'null'}`);
277
397
  return;
278
398
  }
279
399
 
@@ -288,6 +408,20 @@ export class AgentHost {
288
408
  const session = this.sessions.get(laneKey);
289
409
  if (!session?.sessionId) return;
290
410
 
411
+ // Cold-start recovery: restore FSM state from sessionStore when the
412
+ // in-memory lane has no prior context (e.g. after a container restart).
413
+ if (this.sessionStore && session.ctx === undefined && session.state === undefined) {
414
+ try {
415
+ const persisted = await this.sessionStore.load(laneKey);
416
+ if (persisted) {
417
+ session.ctx = persisted.ctx;
418
+ session.state = persisted.state;
419
+ }
420
+ } catch (err: unknown) {
421
+ runtimeLog.warn('AgentHost', `sessionStore.load failed laneKey=${laneKey}: ${(err as Error)?.message || err}`);
422
+ }
423
+ }
424
+
291
425
  const identity: Identity = {
292
426
  agentId: session.ownAgentId || this.options.agentId,
293
427
  sessionId: session.sessionId,
@@ -296,7 +430,7 @@ export class AgentHost {
296
430
 
297
431
  try {
298
432
  const result = await Promise.race([
299
- this.agent.tick({ event, ctx: session.ctx, state: session.state, identity, eff: this.eff, memory: this.memory }),
433
+ this.agent.tick({ event, ctx: session.ctx, state: session.state, identity, eff: this.eff, memory: this.memory, transitionTap: this.options.transitionTap }),
300
434
  new Promise<never>((_, reject) =>
301
435
  setTimeout(() => reject(new Error('tick timeout')), this.tickTimeoutMs),
302
436
  ),
@@ -304,6 +438,12 @@ export class AgentHost {
304
438
  session.ctx = result.ctx;
305
439
  session.state = result.state;
306
440
  this.sessions.set(laneKey, session);
441
+
442
+ if (this.sessionStore) {
443
+ this.sessionStore.save(laneKey, { ctx: session.ctx, state: session.state }).catch((err: unknown) => {
444
+ runtimeLog.warn('AgentHost', `sessionStore.save failed laneKey=${laneKey}: ${(err as Error)?.message || err}`);
445
+ });
446
+ }
307
447
  } catch (err: unknown) {
308
448
  runtimeLog.warn('AgentHost', `tick failed laneKey=${laneKey}: ${(err as Error)?.message || err}`);
309
449
  }
@@ -315,8 +455,21 @@ export class AgentHost {
315
455
  return this.options.agentId || (self?.id as string) || (agent?.id as string) || (metadata.ziggsAgentId as string) || null;
316
456
  }
317
457
 
318
- connectAsync(timeout?: number) { return this.wsClient.connectAsync(timeout); }
458
+ async connectAsync(timeout?: number) {
459
+ await this.wsClient.connectAsync(timeout);
460
+ if (this.inboxCatchUpEnabled) {
461
+ await this._ensureCatchUp();
462
+ // Backend flushPendingMessages can emit after auth; re-poll once so inbox
463
+ // ack catches stragglers and we don't leave newMessages > 0 (ZIG-462).
464
+ await new Promise((r) => setTimeout(r, 750));
465
+ this.catchUpInFlight = null;
466
+ await this._ensureCatchUp();
467
+ }
468
+ return this;
469
+ }
319
470
  disconnect() { this.wsClient.disconnect(); }
471
+ /** Waits for all in-flight ticks to complete. Call before process.exit() on SIGTERM. */
472
+ drain(timeoutMs = 30_000): Promise<void> { return this.eventQueue.waitForIdle(timeoutMs); }
320
473
  registerTool(tool: ToolDefinition) { return this.toolManager.register(tool); }
321
474
  isConnected() { return this.wsClient.isConnected(); }
322
475
  }
@@ -122,4 +122,25 @@ export class OpenAIAdapter {
122
122
  throw error;
123
123
  }
124
124
  }
125
+
126
+ async chatMessagesStream(
127
+ messages: OpenAI.Chat.ChatCompletionMessageParam[],
128
+ onChunk: (text: string) => void,
129
+ ): Promise<string> {
130
+ const stream = await this.client.chat.completions.create({
131
+ model: this.model,
132
+ messages,
133
+ stream: true,
134
+ max_completion_tokens: 3000,
135
+ });
136
+ let text = '';
137
+ for await (const chunk of stream) {
138
+ const delta = chunk.choices[0]?.delta?.content;
139
+ if (delta) {
140
+ text += delta;
141
+ onChunk(delta);
142
+ }
143
+ }
144
+ return text;
145
+ }
125
146
  }
@@ -1,5 +1,5 @@
1
1
  import type { Ctx, Identity, Workflow, EffectHandler } from '../types.js';
2
- import { AgentMachine } from '../runtime/AgentMachine.js';
2
+ import { AgentMachine, type TransitionEvent } from '../runtime/AgentMachine.js';
3
3
  import { runTurn } from '../runtime/runTurn.js';
4
4
  import { PromptBuilder } from '../runtime/PromptBuilder.js';
5
5
  import type { MemoryStore } from '../memory/MemoryStore.js';
@@ -29,6 +29,8 @@ export interface TickInput {
29
29
  * choice — `counterparty:<id>`, `agreement:<id>:notes`, etc.
30
30
  */
31
31
  memory?: MemoryStore;
32
+ /** Optional tap for FSM transition events (ZIG-442). */
33
+ transitionTap?: (event: TransitionEvent) => void;
32
34
  }
33
35
 
34
36
  export interface TickOutput {
@@ -70,7 +72,7 @@ export class Agent {
70
72
  this.promptBuilder = promptBuilder || new PromptBuilder();
71
73
  }
72
74
 
73
- async tick({ event, ctx, state, identity, eff, memory }: TickInput): Promise<TickOutput> {
75
+ async tick({ event, ctx, state, identity, eff, memory, transitionTap }: TickInput): Promise<TickOutput> {
74
76
  if (!identity?.sessionId) throw new Error('Agent.tick: identity.sessionId is required');
75
77
  if (!eff) throw new Error('Agent.tick: eff is required');
76
78
 
@@ -82,6 +84,7 @@ export class Agent {
82
84
  promptBuilder: this.promptBuilder,
83
85
  memory,
84
86
  input: { identity, ctx, state },
87
+ transitionTap,
85
88
  });
86
89
 
87
90
  // `send` awaits the recursive _step until the machine settles at a
@@ -40,7 +40,7 @@ function validateAgreements(agreements: unknown, warn: WarnFn): void {
40
40
  }
41
41
  if (ag['parties'] && typeof ag['parties'] === 'object') {
42
42
  const parties = ag['parties'] as Record<string, unknown>;
43
- for (const key of ['creatorId', 'providerId']) {
43
+ for (const key of ['creator', 'provider']) {
44
44
  if (!parties[key]) warn(`${path}.parties.${key}`, 'missing');
45
45
  }
46
46
  }
@@ -10,8 +10,8 @@ export function isTaskResultRelevantToAgent(result: Record<string, unknown>, own
10
10
  const parties = (result['agreement'] as Record<string, unknown> | undefined)?.['parties'] as Record<string, unknown> | undefined ?? {};
11
11
  return !!(
12
12
  result['creatorIsYou'] || result['providerIsYou'] || result['payerIsYou'] || result['proposedToIsYou'] ||
13
- parties['creatorId'] === ownAgentId || parties['providerId'] === ownAgentId ||
14
- parties['payerId'] === ownAgentId || parties['proposedToId'] === ownAgentId ||
15
- parties['proposedToId'] === 'everyone'
13
+ parties['creator'] === ownAgentId || parties['provider'] === ownAgentId ||
14
+ parties['payer'] === ownAgentId || parties['proposedTo'] === ownAgentId ||
15
+ parties['proposedTo'] === 'everyone'
16
16
  );
17
17
  }
@@ -3,7 +3,7 @@ import { getBatchEvents, isTaskResultRelevantToAgent } from './batch.js';
3
3
 
4
4
  function isTaskAssignment(result: Record<string, unknown>, ownAgentId: string | null): boolean {
5
5
  const parties = (result['agreement'] as Record<string, unknown> | undefined)?.['parties'] as Record<string, unknown> | undefined ?? {};
6
- const isProvider = result['providerIsYou'] || parties['providerId'] === ownAgentId;
6
+ const isProvider = result['providerIsYou'] || parties['provider'] === ownAgentId;
7
7
  return !!(isProvider && (result['state'] === 'active' || result['state'] === 'in-progress'));
8
8
  }
9
9
 
@@ -24,7 +24,7 @@ export function classifyIncomingEvent(rawEvent: unknown, ownAgentId: string | nu
24
24
  const parties = (ag?.['parties'] as Record<string, unknown>) || {};
25
25
  const proposalStatus = (ag?.['proposal'] as Record<string, unknown> | undefined)?.['status'];
26
26
 
27
- if (state === 'active' && proposalStatus === 'approved' && ownAgentId && parties['creatorId'] === ownAgentId) {
27
+ if (state === 'active' && proposalStatus === 'approved' && ownAgentId && parties['creator'] === ownAgentId) {
28
28
  flags['approval'] = true; flags['taskAssignment'] = result; continue;
29
29
  }
30
30
  if (isTaskAssignment(result, ownAgentId)) { flags['taskAssignment'] = result; flags['approval'] = true; continue; }
@@ -24,7 +24,7 @@ export function classifyWorkflowEvent(event: unknown, ownAgentId: string | null
24
24
  const ag = result['agreement'] as Record<string, unknown> | undefined;
25
25
  const parties = (ag?.['parties'] as Record<string, unknown>) || {};
26
26
  const proposalStatus = ag?.['proposal'] ? (ag['proposal'] as Record<string, unknown>)['status'] : undefined;
27
- const isProvider = result['providerIsYou'] || parties['providerId'] === ownAgentId;
27
+ const isProvider = result['providerIsYou'] || parties['provider'] === ownAgentId;
28
28
  if (isProvider && (state === 'active' || state === 'in-progress')) return 'task_assignment';
29
29
  if (proposalStatus === 'approved' || (state === 'active' && proposalStatus === 'pending')) return 'task_approved';
30
30
  if (proposalStatus === 'rejected' || state === 'cancelled') return 'task_rejected';
@@ -32,11 +32,11 @@ export class AgreementFormatter {
32
32
  const parties = (ag['parties'] as AnyObj) || {};
33
33
  const lines = [
34
34
  `${indent}[${status}] Agreement ${agId}`,
35
- `${indent} Creator: ${this._labelParty(parties['creatorId'] as string, ag['creatorIsYou'] as boolean)}`,
36
- `${indent} Provider: ${this._labelParty(parties['providerId'] as string, ag['providerIsYou'] as boolean)}`,
37
- `${indent} Payer: ${this._labelParty(parties['payerId'] as string, ag['payerIsYou'] as boolean)}`,
35
+ `${indent} Creator: ${this._labelParty(parties['creator'] as string, ag['creatorIsYou'] as boolean)}`,
36
+ `${indent} Provider: ${this._labelParty(parties['provider'] as string, ag['providerIsYou'] as boolean)}`,
37
+ `${indent} Payer: ${this._labelParty(parties['payer'] as string, ag['payerIsYou'] as boolean)}`,
38
38
  ];
39
- if (parties['proposedToId']) lines.push(`${indent} Proposed to: ${this._labelParty(parties['proposedToId'] as string, ag['proposedToIsYou'] as boolean)}`);
39
+ if (parties['proposedTo']) lines.push(`${indent} Proposed to: ${this._labelParty(parties['proposedTo'] as string, ag['proposedToIsYou'] as boolean)}`);
40
40
  const proposal = ag['proposal'] as AnyObj | undefined;
41
41
  if (proposal?.['status']) lines.push(`${indent} Proposal: ${proposal['status']}${proposal['respondedBy'] ? ` (by ${proposal['respondedBy']})` : ''}`);
42
42
  const money = ag['money'] as AnyObj | undefined;
@@ -86,7 +86,7 @@ export class AgreementFormatter {
86
86
  for (const st of subtasks) {
87
87
  const sstate = ((st['state'] as string) || 'unknown').toUpperCase();
88
88
  const sid = this.shortId(st['taskId'] as string);
89
- lines.push(`${indent} [${sstate}] ${st['description'] || sid} (${sid}, provider: ${st['providerIsYou'] ? 'You' : (st['providerId'] ?? 'unassigned')})`);
89
+ lines.push(`${indent} [${sstate}] ${st['description'] || sid} (${sid}, provider: ${st['providerIsYou'] ? 'You' : (st['provider'] ?? 'unassigned')})`);
90
90
  }
91
91
  }
92
92
  return lines.join('\n');
@@ -72,7 +72,7 @@ export class HistoryFormatter {
72
72
  case 'created': {
73
73
  const ag = task['agreement'] as AnyObj | undefined;
74
74
  const parties = (ag?.['parties'] as AnyObj) || {};
75
- return `${timePrefix}Task created under agreement: "${desc}" (${ids})\n Owner: ${this.formatOwner(task, agentId)}\n Executor: ${this.formatExecutor(task, agentId)}\n Proposed to: ${parties['proposedToId'] || 'N/A'}`;
75
+ return `${timePrefix}Task created under agreement: "${desc}" (${ids})\n Owner: ${this.formatOwner(task, agentId)}\n Executor: ${this.formatExecutor(task, agentId)}\n Proposed to: ${parties['proposedTo'] || 'N/A'}`;
76
76
  }
77
77
  case 'state_changed': {
78
78
  const prev = (service?.['previousState'] as AnyObj | undefined)?.['state'] || '?';
@@ -124,13 +124,13 @@ export class HistoryFormatter {
124
124
 
125
125
  formatOwner(task: AnyObj, agentId: string): string {
126
126
  const ag = task['agreement'] as AnyObj | undefined;
127
- const creatorId = (ag?.['parties'] as AnyObj | undefined)?.['creatorId'] ?? task['agentId'];
127
+ const creatorId = (ag?.['parties'] as AnyObj | undefined)?.['creator'] ?? task['agentId'];
128
128
  return creatorId === agentId ? 'You' : String(creatorId ?? 'N/A');
129
129
  }
130
130
 
131
131
  formatExecutor(task: AnyObj, agentId: string): string {
132
132
  const ag = task['agreement'] as AnyObj | undefined;
133
- const providerId = (ag?.['parties'] as AnyObj | undefined)?.['providerId'] ?? task['executorId'];
133
+ const providerId = (ag?.['parties'] as AnyObj | undefined)?.['provider'] ?? task['executorId'];
134
134
  return providerId === agentId ? 'You' : String(providerId ?? 'N/A');
135
135
  }
136
136
 
package/src/index.ts CHANGED
@@ -20,7 +20,7 @@ export { OutboxBuffer } from './server/OutboxBuffer.js';
20
20
  export { normalizeIncomingEvent } from './ingress/normalizeIncoming.js';
21
21
 
22
22
  // Runtime primitives
23
- export { AgentMachine } from './runtime/AgentMachine.js';
23
+ export { AgentMachine, type TransitionEvent } from './runtime/AgentMachine.js';
24
24
  export { runTurn } from './runtime/runTurn.js';
25
25
  export { PromptBuilder } from './runtime/PromptBuilder.js';
26
26
  export { thinkingDefaults, defineThinkingState, DEFAULT_WAIT_PROMPT } from './runtime/defaults.js';
@@ -54,21 +54,38 @@ export { AgreementService } from './server/agreements/AgreementService.js';
54
54
  export { ToolManager } from './tools/ToolManager.js';
55
55
  export { ToolProvider } from './tools/ToolProvider.js';
56
56
  export { defineTool } from './tools/defineTool.js';
57
+ export { recordReport } from './tools/recordReport.js';
58
+ export type { RecordReportInput, RecordReportResult, TaskReport } from './tools/recordReport.js';
57
59
  export { OpenAIAdapter } from './adapters/OpenAIAdapter.js';
58
60
  export { extractJSON, safeParseJSON } from './utils/jsonExtractor.js';
59
61
  export { HistoryFormatter, historyFormatter, AgreementFormatter, agreementFormatter } from './formatters/index.js';
60
62
  export { ProactiveTrigger } from './server/proactive/ProactiveTrigger.js';
61
63
  export { ZiggsPayClient, createZiggsPayClient } from './server/ziggspay/ZiggsPayClient.js';
64
+ export { ZiggsConnectClient, createZiggsConnectClient } from './server/ziggsconnect/ZiggsConnectClient.js';
65
+ export { ZiggsContextClient } from './server/ziggscontext/ZiggsContextClient.js';
66
+ export type {
67
+ ZiggsContextClientOptions,
68
+ ContextReachDescriptor,
69
+ DelegateContextGrantParams,
70
+ } from './server/ziggscontext/ZiggsContextClient.js';
71
+ // All agent tool bundles live under server/tools/ (tier1 = protocol grammar,
72
+ // tier2 = opt-in HTTP capability bundles). Single home: server/tools/index.ts.
62
73
  export {
74
+ // Tier 1 — protocol grammar (framework-managed via taskTools).
63
75
  PROTOCOL_TOOLS, agreementProposeTool, agreementSubcontractTool,
64
76
  agreementRespondTool, agreementCounterProposalTool, agreementCheckProposalTool,
65
77
  taskSpawnTool, taskUpdateTool, taskUpdatePlanStepTool,
66
- } from './server/tasks/protocolTools.js';
67
- export { PAYMENT_TOOLS, paymentBalanceTool, paymentTransferTool, paymentHoldTool, paymentReleaseTool, paymentResolveWalletTool } from './server/tasks/paymentTools.js';
78
+ dispatchProtocolOp,
79
+ // Tier 2 opt-in capability bundles (spread into `tools:`).
80
+ PAYMENT_TOOLS, paymentBalanceTool, paymentTransferTool, paymentHoldTool, paymentReleaseTool, paymentResolveWalletTool,
81
+ CONNECTION_TOOLS, connectionProxyTool, connectionListGrantsTool,
82
+ DISCOVERY_TOOLS, agentSearchTool, agentGetTool,
83
+ MARKETPLACE_TOOLS, marketplaceViewTool,
84
+ } from './server/tools/index.js';
68
85
  export { PROTOCOL_TOOL_NAMES, PROTOCOL_TOOL_TO_OPERATION, mapProtocolToolToOperation, isProtocolToolName } from './tasks/protocolRegistry.js';
69
- export { dispatchProtocolOp } from './server/tasks/protocolRunner.js';
70
86
  export { InMemoryStore, FileMemoryStore } from './memory/MemoryStore.js';
71
87
  export type { MemoryStore } from './memory/MemoryStore.js';
88
+ export type { SessionStore } from './AgentHost.js';
72
89
  export { classifyIncomingEvent } from './context/classifyEnvelope.js';
73
90
  export { buildContextUpdates, CONTEXT_RESET } from './context/applyEffects.js';
74
91
  export { getBatchEvents, isTaskResultRelevantToAgent } from './context/batch.js';
@@ -83,4 +100,8 @@ export {
83
100
  getWebSocketUrl,
84
101
  } from '@ziggs-ai/api-client';
85
102
 
103
+ export { runInboxCatchUp } from './server/InboxCatchUp.js';
104
+ export type { InboxCatchUpDeps, InboxCatchUpResult } from './server/InboxCatchUp.js';
105
+ export { SeenMessages } from './server/SeenMessages.js';
86
106
  export { runtimeLog, resetRuntimeLogLevelCache } from './shared/runtimeLog.js';
107
+ export type { RuntimeLogLevel } from './shared/runtimeLog.js';