@ziggs-ai/agent-sdk 0.14.0 → 0.16.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (36) hide show
  1. package/dist/AgentHost.d.ts +23 -2
  2. package/dist/AgentHost.js +102 -2
  3. package/dist/adapters/McpAdapter.js +1 -1
  4. package/dist/agent/ClaudeSdkAgent.js +54 -4
  5. package/dist/agent/claudeSdkProtocol.d.ts +1 -1
  6. package/dist/agent/claudeSdkProtocol.js +2 -2
  7. package/dist/agent/claudeSdkTools.d.ts +2 -1
  8. package/dist/agent/claudeSdkTools.js +29 -58
  9. package/dist/index.d.ts +3 -2
  10. package/dist/index.js +2 -2
  11. package/dist/pricing/fleetTierByAgentId.gen.d.ts +1 -1
  12. package/dist/pricing/fleetTierByAgentId.gen.js +3 -8
  13. package/dist/runtime/AgentMachine.d.ts +28 -1
  14. package/dist/runtime/AgentMachine.js +55 -16
  15. package/dist/runtime/buildOutcome.d.ts +0 -8
  16. package/dist/runtime/buildOutcome.js +19 -5
  17. package/dist/runtime/kickoff.d.ts +23 -1
  18. package/dist/runtime/kickoff.js +45 -3
  19. package/dist/runtime/wakeOutcome.d.ts +27 -0
  20. package/dist/runtime/wakeOutcome.js +43 -0
  21. package/dist/server/ConnectionPool.js +11 -3
  22. package/dist/server/EventQueue.d.ts +23 -2
  23. package/dist/server/EventQueue.js +33 -3
  24. package/dist/server/agreements/AgreementService.d.ts +10 -1
  25. package/dist/server/agreements/AgreementService.js +9 -1
  26. package/dist/server/tools/index.d.ts +2 -0
  27. package/dist/server/tools/index.js +3 -0
  28. package/dist/server/tools/tier1/protocolRunner.js +33 -27
  29. package/dist/server/tools/tier1/protocolTools.js +2 -7
  30. package/dist/server/tools/tier2/introductionTools.d.ts +6 -0
  31. package/dist/server/tools/tier2/introductionTools.js +12 -0
  32. package/dist/server/tools/tier2/taskTools.d.ts +3 -0
  33. package/dist/server/tools/tier2/taskTools.js +6 -0
  34. package/dist/tools/recordReport.d.ts +5 -0
  35. package/dist/tools/recordReport.js +5 -0
  36. package/package.json +2 -2
@@ -32,6 +32,10 @@ export type TransitionEvent = {
32
32
  kind: 'error-escape';
33
33
  fromState: string;
34
34
  toState: string;
35
+ } | {
36
+ kind: 'unmatched-escape';
37
+ fromState: string;
38
+ toState: string;
35
39
  } | {
36
40
  kind: 'timeout-unhandled';
37
41
  state: string;
@@ -134,8 +138,31 @@ export declare class AgentMachine {
134
138
  */
135
139
  send(frame: IncomingFrame): Promise<void>;
136
140
  _handleFrame(frame: IncomingFrame): Promise<void>;
141
+ /**
142
+ * Give up on a wedged state and go back to listening.
143
+ *
144
+ * Both runaway guards used to just `return`, which left `this.state` AT the
145
+ * state that had run away. Parking inside a thinking state is not parking —
146
+ * it is a lane stuck in a room whose transitions were written for one
147
+ * occasion, so every later event is evaluated against the wrong state and
148
+ * anything that state does not route is swallowed. Measured: three lab
149
+ * workers tripped the depth guard inside the injected `claimKickingOff`
150
+ * greeting; `claimKickingOff` has no `task-assigned` route, so the task wake
151
+ * that arrived next was dropped and three graph nodes were never worked at
152
+ * all. The greeting is best-effort; the work is not.
153
+ *
154
+ * The initial state is the one the author wrote to receive anything, so it is
155
+ * the only safe place to land.
156
+ */
157
+ private _parkAtInitial;
137
158
  _step(outcome: Outcome, depth?: number): Promise<void>;
138
- /** Error with no author-defined escape: park at workflow.initial if it is parked. */
159
+ /**
160
+ * Unmatched thinking outcome: go back to workflow.initial when that state is
161
+ * parked. Used to require an error outcome, so a tool-result /
162
+ * message-sent / wait with no route logged "parking" and left `this.state`
163
+ * on the thinking state. The next event then ran a turn against prompts
164
+ * written for the previous occasion.
165
+ */
139
166
  _escapeToInitialParked(outcome: Outcome, fromState: string): Promise<boolean>;
140
167
  /** Sends a short fallback to the last known human user when the machine parks unexpectedly. Best-effort — never throws. */
141
168
  _sendFallbackIfUserWaiting(text: string): Promise<void>;
@@ -144,6 +144,30 @@ export class AgentMachine {
144
144
  applyOutcomeToCtx(this.ctx, outcome);
145
145
  await this._step(outcome);
146
146
  }
147
+ /**
148
+ * Give up on a wedged state and go back to listening.
149
+ *
150
+ * Both runaway guards used to just `return`, which left `this.state` AT the
151
+ * state that had run away. Parking inside a thinking state is not parking —
152
+ * it is a lane stuck in a room whose transitions were written for one
153
+ * occasion, so every later event is evaluated against the wrong state and
154
+ * anything that state does not route is swallowed. Measured: three lab
155
+ * workers tripped the depth guard inside the injected `claimKickingOff`
156
+ * greeting; `claimKickingOff` has no `task-assigned` route, so the task wake
157
+ * that arrived next was dropped and three graph nodes were never worked at
158
+ * all. The greeting is best-effort; the work is not.
159
+ *
160
+ * The initial state is the one the author wrote to receive anything, so it is
161
+ * the only safe place to land.
162
+ */
163
+ _parkAtInitial() {
164
+ const initial = this.workflow.initial;
165
+ if (this.state !== initial) {
166
+ runtimeLog.warn('AgentMachine', `recovering wedged state=${this.state} → ${initial}`);
167
+ this.state = initial;
168
+ }
169
+ this._notify();
170
+ }
147
171
  async _step(outcome, depth = 0) {
148
172
  const fromState = this.state;
149
173
  const target = this._evaluateTransitions(outcome);
@@ -182,14 +206,14 @@ export class AgentMachine {
182
206
  runtimeLog.warn('AgentMachine', `state=${this.state} runaway mechanical loop (depth=${depth}); parking`);
183
207
  this._transitionTap?.({ kind: 'depth-guard', state: this.state, depth });
184
208
  await this._sendFallbackIfUserWaiting('Something went wrong. Please try again.');
185
- this._notify();
209
+ this._parkAtInitial();
186
210
  return;
187
211
  }
188
212
  if (this._turnsSinceUserInput >= this._maxTurnsWithoutUserInput) {
189
213
  runtimeLog.warn('AgentMachine', `state=${this.state} runaway loop: ${this._turnsSinceUserInput} turns without user input (cap ${this._maxTurnsWithoutUserInput}); parking`);
190
214
  this._transitionTap?.({ kind: 'turn-budget-guard', state: this.state, turns: this._turnsSinceUserInput });
191
215
  await this._sendFallbackIfUserWaiting('I got stuck in a loop and paused. Please send another message to continue.');
192
- this._notify();
216
+ this._parkAtInitial();
193
217
  return;
194
218
  }
195
219
  runtimeLog.debug('FSM', `_step mechanical state=${this.state} — run tool`);
@@ -238,16 +262,23 @@ export class AgentMachine {
238
262
  return;
239
263
  }
240
264
  // A thinking turn that yields an outcome no transition handles must not
241
- // silently re-enter the same state and burn another LLM call.
242
- // Explicit self-loops still match (target is non-null, even if target === state).
265
+ // silently re-enter the same state and burn another LLM call, and must
266
+ // not stay IN that thinking state — the next event would be judged by
267
+ // transitions written for a different occasion. Explicit
268
+ // self-loops still match (target is non-null, even if target === state).
243
269
  if (sd.kind === 'thinking' &&
244
270
  depth > 0 &&
245
271
  fromState === this.state &&
246
272
  !target) {
273
+ this._transitionTap?.({
274
+ kind: 'no-match',
275
+ state: fromState,
276
+ outcomeKind: outcome.kind,
277
+ depth,
278
+ });
247
279
  if (await this._escapeToInitialParked(outcome, fromState))
248
280
  return;
249
281
  runtimeLog.warn('AgentMachine', `state=${fromState} outcome.kind=${outcome.kind} matched no transition; parking (depth=${depth})`);
250
- this._transitionTap?.({ kind: 'no-match', state: fromState, outcomeKind: outcome.kind, depth });
251
282
  await this._sendFallbackIfUserWaiting('Something went wrong. Please try again.');
252
283
  this._notify();
253
284
  return;
@@ -265,7 +296,7 @@ export class AgentMachine {
265
296
  runtimeLog.warn('AgentMachine', `state=${this.state} runaway turn loop (depth=${depth}); parking`);
266
297
  this._transitionTap?.({ kind: 'depth-guard', state: this.state, depth });
267
298
  await this._sendFallbackIfUserWaiting('Something went wrong. Please try again.');
268
- this._notify();
299
+ this._parkAtInitial();
269
300
  return;
270
301
  }
271
302
  // Cross-event runaway guard. The depth-guard above only bounds
@@ -278,7 +309,7 @@ export class AgentMachine {
278
309
  runtimeLog.warn('AgentMachine', `state=${this.state} runaway loop: ${this._turnsSinceUserInput} turns without user input (cap ${this._maxTurnsWithoutUserInput}); parking`);
279
310
  this._transitionTap?.({ kind: 'turn-budget-guard', state: this.state, turns: this._turnsSinceUserInput });
280
311
  await this._sendFallbackIfUserWaiting('I got stuck in a loop and paused. Please send another message to continue.');
281
- this._notify();
312
+ this._parkAtInitial();
282
313
  return;
283
314
  }
284
315
  runtimeLog.debug('FSM', `_step thinking state=${this.state} — run turn`);
@@ -320,8 +351,8 @@ export class AgentMachine {
320
351
  applyOutcomeToCtx(this.ctx, errOutcome);
321
352
  this._running = false;
322
353
  this._notify();
323
- // Try to fall through using current state's transitions; if none match
324
- // the error outcome the machine just parks (next event will re-enter).
354
+ // Try to fall through using current state's transitions; if none match,
355
+ // recover to workflow.initial so the next event is not judged here.
325
356
  const fallback = this._evaluateTransitions(errOutcome);
326
357
  if (fallback && fallback !== this.state) {
327
358
  this.state = fallback;
@@ -329,23 +360,31 @@ export class AgentMachine {
329
360
  }
330
361
  else {
331
362
  await this._sendFallbackIfUserWaiting('Something went wrong. Please try again.');
363
+ this._parkAtInitial();
332
364
  }
333
365
  }
334
366
  }
335
- /** Error with no author-defined escape: park at workflow.initial if it is parked. */
367
+ /**
368
+ * Unmatched thinking outcome: go back to workflow.initial when that state is
369
+ * parked. Used to require an error outcome, so a tool-result /
370
+ * message-sent / wait with no route logged "parking" and left `this.state`
371
+ * on the thinking state. The next event then ran a turn against prompts
372
+ * written for the previous occasion.
373
+ */
336
374
  async _escapeToInitialParked(outcome, fromState) {
337
- if (outcome.kind !== 'error')
338
- return false;
339
375
  const initial = this.workflow.initial;
340
376
  const initialSd = this.workflow.states[initial];
341
377
  if (!initialSd || initialSd.kind !== 'parked' || initial === fromState) {
342
378
  return false;
343
379
  }
344
- runtimeLog.warn('AgentMachine', `state=${fromState} error outcome had no transition — escaped to parked initial "${initial}"`);
345
- this._transitionTap?.({ kind: 'error-escape', fromState, toState: initial });
380
+ const kind = outcome.kind;
381
+ runtimeLog.warn('AgentMachine', `state=${fromState} unmatched ${String(kind)} — escaped to parked initial "${initial}"`);
382
+ this._transitionTap?.({
383
+ kind: kind === 'error' ? 'error-escape' : 'unmatched-escape',
384
+ fromState,
385
+ toState: initial,
386
+ });
346
387
  this.state = initial;
347
- // The turn failed and no workflow transition handled it — a waiting human
348
- // got nothing, so apologize before parking (same text as the other parks).
349
388
  await this._sendFallbackIfUserWaiting('Something went wrong. Please try again.');
350
389
  this._notify();
351
390
  return true;
@@ -36,14 +36,6 @@ export interface EmittedEvent {
36
36
  senderId?: string;
37
37
  [key: string]: unknown;
38
38
  }
39
- /**
40
- * A chat message whose stated receiver is a DIFFERENT party. `receiverId` is
41
- * the sender's addressing decision, carried on every wire frame; the
42
- * human-broadcast sentinel counts as "elsewhere" for an agent (a broadcast is
43
- * addressed to the room's humans). A frame with NO receiver keeps the turn —
44
- * we cannot know it is not ours, and skipping on absence would deafen agents
45
- * to malformed or legacy frames.
46
- */
47
39
  export declare function isMessageAddressedElsewhere(ev: RawEvent | null | undefined, ownAgentId: string | null | undefined): boolean;
48
40
  /**
49
41
  * Converts a raw incoming event into the single Outcome that re-enters the
@@ -9,12 +9,26 @@ import { bookForDelegation, ensureTaskBook, findBookHoldingPending, removeTaskBo
9
9
  * we cannot know it is not ours, and skipping on absence would deafen agents
10
10
  * to malformed or legacy frames.
11
11
  */
12
+ function isSyntheticOrgReceiver(id) {
13
+ if (/^([a-z0-9][a-z0-9-]*)\1@ziggs\.local$/i.test(id))
14
+ return true;
15
+ const sep = id.indexOf(':::');
16
+ if (sep > 0)
17
+ return /@ziggs\.local$/i.test(id.slice(sep + 3));
18
+ return false;
19
+ }
12
20
  export function isMessageAddressedElsewhere(ev, ownAgentId) {
13
- return Boolean(ev &&
14
- ev.type === 'message' &&
15
- ownAgentId &&
16
- ev.receiverId &&
17
- ev.receiverId !== ownAgentId);
21
+ if (!ev || ev.type !== 'message' || !ownAgentId || !ev.receiverId) {
22
+ return false;
23
+ }
24
+ if (ev.receiverId === ownAgentId)
25
+ return false;
26
+ // Fleet synthetic users are not a party that will answer. A hire room
27
+ // born with the org ghost as the other face used to address the human's
28
+ // message there, and the agent skipped the turn — claimed, acked, silence.
29
+ if (isSyntheticOrgReceiver(String(ev.receiverId)))
30
+ return false;
31
+ return true;
18
32
  }
19
33
  // ── outcomeFromEvent: incoming wire event → Outcome ──────────────────────────
20
34
  /**
@@ -1,4 +1,4 @@
1
- import type { ThinkingState, Workflow } from '../types.js';
1
+ import type { ThinkingState, Transition, Workflow } from '../types.js';
2
2
  /** Reserved state name for the injected agreement-kickoff default (proposer). */
3
3
  export declare const KICKOFF_STATE = "kickingOff";
4
4
  /**
@@ -8,6 +8,28 @@ export declare const KICKOFF_STATE = "kickingOff";
8
8
  export declare const CLAIM_KICKOFF_STATE = "claimKickingOff";
9
9
  export declare function buildKickoffState(initial: string): ThinkingState;
10
10
  /** provider side after a store/listing claim. */
11
+ /**
12
+ * Declare that THIS state answers a store-claim wake itself, so the injected
13
+ * `claimKickingOff` greeting is not added to it.
14
+ *
15
+ * The opt-out has to fire on a claim wake and NOT on a bare counterparty
16
+ * approve — `stateHandlesClaimKickoff` uses that difference to tell a
17
+ * deliberate claim handler from a generic `proposal-resolved` route that
18
+ * happens to match. Writing that predicate by hand is easy to get subtly wrong,
19
+ * and getting it wrong is silent: the agent greets on every claim while its
20
+ * author believes it opted out. That cost one lab cast 100% of its token spend
21
+ * and three wedged workers before anyone noticed.
22
+ *
23
+ * transitions: [
24
+ * handlesClaimWake('idle'), // a claim does not make this agent talk
25
+ * { to: 'idle', when: (o) => o.kind === 'proposal-resolved' && o.action === 'approve' },
26
+ * ]
27
+ *
28
+ * Note the second route is still worth keeping when you also want to sit out
29
+ * the PROPOSER-side kickoff: that one probes with a bare approve, which this
30
+ * helper deliberately does not match.
31
+ */
32
+ export declare function handlesClaimWake(to: string): Transition;
11
33
  export declare function buildClaimKickoffState(initial: string): ThinkingState;
12
34
  /**
13
35
  * Agreement-kickoff defaults. For every state whose transitions do not already
@@ -143,6 +143,33 @@ export function buildKickoffState(initial) {
143
143
  };
144
144
  }
145
145
  /** provider side after a store/listing claim. */
146
+ /**
147
+ * Declare that THIS state answers a store-claim wake itself, so the injected
148
+ * `claimKickingOff` greeting is not added to it.
149
+ *
150
+ * The opt-out has to fire on a claim wake and NOT on a bare counterparty
151
+ * approve — `stateHandlesClaimKickoff` uses that difference to tell a
152
+ * deliberate claim handler from a generic `proposal-resolved` route that
153
+ * happens to match. Writing that predicate by hand is easy to get subtly wrong,
154
+ * and getting it wrong is silent: the agent greets on every claim while its
155
+ * author believes it opted out. That cost one lab cast 100% of its token spend
156
+ * and three wedged workers before anyone noticed.
157
+ *
158
+ * transitions: [
159
+ * handlesClaimWake('idle'), // a claim does not make this agent talk
160
+ * { to: 'idle', when: (o) => o.kind === 'proposal-resolved' && o.action === 'approve' },
161
+ * ]
162
+ *
163
+ * Note the second route is still worth keeping when you also want to sit out
164
+ * the PROPOSER-side kickoff: that one probes with a bare approve, which this
165
+ * helper deliberately does not match.
166
+ */
167
+ export function handlesClaimWake(to) {
168
+ return {
169
+ to,
170
+ when: (o) => o.kind === 'proposal-resolved' && o.action === 'approve' && o.fromClaimWake === true,
171
+ };
172
+ }
146
173
  export function buildClaimKickoffState(initial) {
147
174
  const dflt = thinkingDefaults({ initial });
148
175
  return {
@@ -158,15 +185,30 @@ export function buildClaimKickoffState(initial) {
158
185
  prompt: {
159
186
  when: 'There is not yet a real chat with the buyer (common on store claims).',
160
187
  instruction: 'Open or reuse a chat with the buyer who claimed the listing.',
161
- format: '{"participantId": "<buyer actorId / Sender from the event>"}',
188
+ format: '{"participantId": "<buyer actorId / Sender from the event>", "agreementId": "<the claimed agreement id>"}',
162
189
  },
163
190
  tool: 'chat_open',
164
191
  produces: (result) => {
165
192
  const r = result;
166
193
  return r?.chatId ? 'tool-result' : 'error';
167
194
  },
168
- // Stay in this state so the next turn can greet in the opened chat.
169
- terminatesLoop: true,
195
+ /**
196
+ * Deliberately does NOT terminate the turn.
197
+ *
198
+ * It used to, with "stay in this state so the next turn can greet in the
199
+ * opened chat" — but the next turn re-enters this state from scratch,
200
+ * reads the same prompt ("there is not yet a real chat"), and opens the
201
+ * chat again. Nothing in a re-entered state says a chat is already open:
202
+ * `chat_open` is idempotent, so every retry succeeds, produces
203
+ * `tool-result`, and re-enters. Measured: three lab workers called
204
+ * `chat_open` twelve times each, sent zero messages, and tripped the
205
+ * depth guard — the greeting never happened and the wake was spent.
206
+ *
207
+ * Letting the turn continue puts the chatId in front of the model as a
208
+ * tool result, so `greetBuyer` runs in the SAME turn. `message_sent`
209
+ * outranks `tool_result` when the outcome is picked, so the turn leaves
210
+ * on `message-sent` → initial: one turn, one greeting, no re-entry.
211
+ */
170
212
  },
171
213
  greetBuyer: {
172
214
  prompt: {
@@ -0,0 +1,27 @@
1
+ /**
2
+ * Every claimed wake must leave a recorded outcome.
3
+ *
4
+ * Claim + ack with no externally visible effect used to exist: the host
5
+ * consumed the delivery, the agent stayed silent, and metering had no row.
6
+ * A wake ends in exactly one of: a reply, task progress, or an explicit
7
+ * no-op with a reason.
8
+ */
9
+ export type WakeOutcomeKind = 'replied' | 'task-progress' | 'no-op';
10
+ export interface WakeOutcome {
11
+ kind: WakeOutcomeKind;
12
+ reason?: string;
13
+ }
14
+ export declare function classifyWakeOutcome(input: {
15
+ skippedReason?: string;
16
+ lastOutcomeKind?: string | null;
17
+ llmCalls: number;
18
+ sentMessage?: boolean;
19
+ taskWrite?: boolean;
20
+ }): WakeOutcome;
21
+ export declare function buildWakeOutcomePayload(input: {
22
+ agentId: string;
23
+ sessionId: string;
24
+ laneKey: string;
25
+ outcome: WakeOutcome;
26
+ llmCalls?: number;
27
+ }): Record<string, unknown>;
@@ -0,0 +1,43 @@
1
+ /**
2
+ * Every claimed wake must leave a recorded outcome.
3
+ *
4
+ * Claim + ack with no externally visible effect used to exist: the host
5
+ * consumed the delivery, the agent stayed silent, and metering had no row.
6
+ * A wake ends in exactly one of: a reply, task progress, or an explicit
7
+ * no-op with a reason.
8
+ */
9
+ export function classifyWakeOutcome(input) {
10
+ if (input.skippedReason) {
11
+ return { kind: 'no-op', reason: input.skippedReason };
12
+ }
13
+ if (input.sentMessage || input.lastOutcomeKind === 'message-sent') {
14
+ return { kind: 'replied' };
15
+ }
16
+ if (input.taskWrite ||
17
+ input.lastOutcomeKind === 'task-delegated' ||
18
+ input.lastOutcomeKind === 'task-completed' ||
19
+ input.lastOutcomeKind === 'task-progress') {
20
+ return { kind: 'task-progress' };
21
+ }
22
+ if (input.llmCalls > 0) {
23
+ return { kind: 'no-op', reason: 'llm_no_visible_effect' };
24
+ }
25
+ return { kind: 'no-op', reason: 'no_llm' };
26
+ }
27
+ export function buildWakeOutcomePayload(input) {
28
+ return {
29
+ source: 'agent',
30
+ chatId: input.sessionId,
31
+ sessionId: input.sessionId,
32
+ execution: {
33
+ llmCallsTotal: input.llmCalls ?? 0,
34
+ },
35
+ meta: {
36
+ effect: 'wake-outcome',
37
+ kind: input.outcome.kind,
38
+ ...(input.outcome.reason ? { reason: input.outcome.reason } : {}),
39
+ laneKey: input.laneKey,
40
+ agentId: input.agentId,
41
+ },
42
+ };
43
+ }
@@ -104,9 +104,17 @@ export class ConnectionPool {
104
104
  async shutdown(timeoutMs = 30_000) {
105
105
  const hosts = [...this.hosts.values()];
106
106
  await this.manager.stopAll();
107
- await Promise.all(hosts.map((host) => host.drain(timeoutMs).catch((err) => {
108
- runtimeLog.warn('ConnectionPool', `drain did not finish: ${err.message}`);
109
- })));
107
+ await Promise.all(hosts.map(async (host) => {
108
+ try {
109
+ const result = await host.drain(timeoutMs);
110
+ if (result?.timedOut) {
111
+ runtimeLog.warn('ConnectionPool', `drain timed out after ${timeoutMs}ms: ${result.queuedEvents} event(s) queued, ${result.busyLanes.length} busy lane(s)`);
112
+ }
113
+ }
114
+ catch (err) {
115
+ runtimeLog.warn('ConnectionPool', `drain did not finish: ${err.message}`);
116
+ }
117
+ }));
110
118
  }
111
119
  /**
112
120
  * Deliver a message to the named agent and wait for its first outbound
@@ -2,6 +2,19 @@ export type ProcessEventFn = (event: unknown, laneKey: string) => Promise<void>;
2
2
  export interface EventQueueOptions {
3
3
  maxQueueLengthPerLane?: number;
4
4
  }
5
+ export interface EventQueueBusyLane {
6
+ laneKey: string;
7
+ queued: number;
8
+ processing: boolean;
9
+ }
10
+ /** Outcome of `waitForIdle` / `AgentHost.drain`. Timeout is data, not a throw. */
11
+ export interface WaitForIdleResult {
12
+ idle: boolean;
13
+ timedOut: boolean;
14
+ timeoutMs: number;
15
+ queuedEvents: number;
16
+ busyLanes: EventQueueBusyLane[];
17
+ }
5
18
  /**
6
19
  * Serializes events per lane and coalesces multiple queued events into one
7
20
  * batch dispatch. The runtime sees at most one in-flight processEvent call
@@ -25,8 +38,16 @@ export declare class EventQueue {
25
38
  private _processLoop;
26
39
  private _trimQueueIfNeeded;
27
40
  get isIdle(): boolean;
28
- /** Resolves when all lanes finish processing, or after timeoutMs. Best-effort — never throws. */
29
- waitForIdle(timeoutMs?: number): Promise<void>;
41
+ /** Lanes still holding work — a turn in flight, queued events, or both. */
42
+ busyLanes(): EventQueueBusyLane[];
43
+ /**
44
+ * Resolves when every lane is idle, or when `timeoutMs` elapses.
45
+ *
46
+ * Never throws. A timeout is returned as data so the caller can name it
47
+ * (queued count, which lanes) instead of a stray log line the verdict never
48
+ * sees. `timeoutMs <= 0` checks once and returns immediately.
49
+ */
50
+ waitForIdle(timeoutMs?: number): Promise<WaitForIdleResult>;
30
51
  getMetrics(): {
31
52
  lanes: number;
32
53
  queuedEvents: number;
@@ -93,15 +93,45 @@ export class EventQueue {
93
93
  get isIdle() {
94
94
  return [...this._state.values()].every(s => !s.processing && s.events.length === 0);
95
95
  }
96
- /** Resolves when all lanes finish processing, or after timeoutMs. Best-effort — never throws. */
96
+ /** Lanes still holding work — a turn in flight, queued events, or both. */
97
+ busyLanes() {
98
+ const out = [];
99
+ for (const [laneKey, state] of this._state) {
100
+ if (!state.processing && state.events.length === 0)
101
+ continue;
102
+ out.push({ laneKey, queued: state.events.length, processing: state.processing });
103
+ }
104
+ return out;
105
+ }
106
+ /**
107
+ * Resolves when every lane is idle, or when `timeoutMs` elapses.
108
+ *
109
+ * Never throws. A timeout is returned as data so the caller can name it
110
+ * (queued count, which lanes) instead of a stray log line the verdict never
111
+ * sees. `timeoutMs <= 0` checks once and returns immediately.
112
+ */
97
113
  async waitForIdle(timeoutMs = 30_000) {
114
+ const snapshot = (timedOut) => {
115
+ const busy = this.busyLanes();
116
+ return {
117
+ idle: busy.length === 0,
118
+ timedOut,
119
+ timeoutMs,
120
+ queuedEvents: busy.reduce((n, lane) => n + lane.queued, 0),
121
+ busyLanes: busy,
122
+ };
123
+ };
124
+ if (this.isIdle)
125
+ return snapshot(false);
126
+ if (timeoutMs <= 0)
127
+ return snapshot(true);
98
128
  const deadline = Date.now() + timeoutMs;
99
129
  while (Date.now() < deadline) {
100
130
  if (this.isIdle)
101
- return;
131
+ return snapshot(false);
102
132
  await new Promise(r => setTimeout(r, 50));
103
133
  }
104
- runtimeLog.warn('EventQueue', `waitForIdle timed out after ${timeoutMs}ms`);
134
+ return snapshot(!this.isIdle);
105
135
  }
106
136
  getMetrics() {
107
137
  return {
@@ -1,4 +1,4 @@
1
- import { type Agreement, type ClaimedKind, type ClaimOptions, type ProposeAgreementData, type ProposeDirectInput, type ProposeBroadcastInput, type PublishOfferPayload, type DelegateAgreementData, type CounterAgreementData, type CreateLinkBody, type CreateLinkResult, type ListAgreementsFilters, type GetMyAgreementsFilters, type ChatLinkType, type ArtifactLinkType } from '@ziggs-ai/api-client';
1
+ import { type Agreement, type ClaimedKind, type ClaimOptions, type ProposeAgreementData, type ProposeDirectInput, type ProposeBroadcastInput, type PublishOfferPayload, type UnifiedProposeInput, type ProposeShape, type DelegateAgreementData, type CounterAgreementData, type CreateLinkBody, type CreateLinkResult, type ListAgreementsFilters, type GetMyAgreementsFilters, type ChatLinkType, type ArtifactLinkType } from '@ziggs-ai/api-client';
2
2
  export declare class AgreementService {
3
3
  private creds;
4
4
  constructor(operatorKey: string, agentId: string);
@@ -12,6 +12,15 @@ export declare class AgreementService {
12
12
  proposeBroadcast(payload: ProposeBroadcastInput): Promise<Agreement>;
13
13
  /** @deprecated Alias for {@link proposeDirect}. */
14
14
  propose(payload: ProposeAgreementData): Promise<Agreement>;
15
+ /**
16
+ * One propose grammar (direct / request / standing offer). Fleet pricing
17
+ * and the wake idempotency key stay on the caller — this is the shared
18
+ * routing, not a second copy of it.
19
+ */
20
+ proposeUnified(input: UnifiedProposeInput): Promise<{
21
+ agreement: Agreement;
22
+ shape: ProposeShape;
23
+ }>;
15
24
  /**
16
25
  * Seller-broadcast marketplace offer. `payload.audience` sets the `payer`
17
26
  * sentinel: 'everyone' (default, public) or 'org' (your active org only).
@@ -1,4 +1,4 @@
1
- import { proposeAgreement, proposeBroadcast as proposeBroadcastApi, delegateAgreement, respondToAgreement, counterAgreement, claimOpenAgreement, getAgreementStatus, listAgreements, getMyAgreements, getAgreement, publishOffer as publishOfferApi, createLink, revokeAgreement, fulfillAgreement, linkAgreementToChat, getChatsForAgreement, linkArtifactToAgreement, getArtifactsForAgreement, } from '@ziggs-ai/api-client';
1
+ import { proposeAgreement, proposeBroadcast as proposeBroadcastApi, delegateAgreement, respondToAgreement, counterAgreement, claimOpenAgreement, proposeUnified, getAgreementStatus, listAgreements, getMyAgreements, getAgreement, publishOffer as publishOfferApi, createLink, revokeAgreement, fulfillAgreement, linkAgreementToChat, getChatsForAgreement, linkArtifactToAgreement, getArtifactsForAgreement, } from '@ziggs-ai/api-client';
2
2
  export class AgreementService {
3
3
  creds;
4
4
  constructor(operatorKey, agentId) {
@@ -24,6 +24,14 @@ export class AgreementService {
24
24
  propose(payload) {
25
25
  return proposeAgreement(payload, this.creds);
26
26
  }
27
+ /**
28
+ * One propose grammar (direct / request / standing offer). Fleet pricing
29
+ * and the wake idempotency key stay on the caller — this is the shared
30
+ * routing, not a second copy of it.
31
+ */
32
+ proposeUnified(input) {
33
+ return proposeUnified(input, this.creds);
34
+ }
27
35
  /**
28
36
  * Seller-broadcast marketplace offer. `payload.audience` sets the `payer`
29
37
  * sentinel: 'everyone' (default, public) or 'org' (your active org only).
@@ -23,7 +23,9 @@ export { PAYMENT_TOOLS, paymentBalanceTool } from './tier2/paymentTools.js';
23
23
  export { CONNECTION_TOOLS, connectionProxyTool, connectionListGrantsTool, requestConnectionTool, } from './tier2/connectionTools.js';
24
24
  export { CONTEXT_TOOLS, contextReadTool, contextDelegateTool, contextExpandReachTool, contextDiscoverGrantableTool, } from './tier2/contextTools.js';
25
25
  export { GRANTS_TOOLS, listGrantsTool } from './tier2/grantsTools.js';
26
+ export { TASK_TOOLS, listTasksTool } from './tier2/taskTools.js';
26
27
  export { LINK_TOOLS, linkProposeTool, linkListTool, } from './tier2/linkTools.js';
28
+ export { INTRODUCTION_TOOLS, introductionMintTool, introductionRedeemTool, introductionListTool, introductionRevokeTool, } from './tier2/introductionTools.js';
27
29
  export { ARTIFACT_TOOLS, recordArtifactTool } from './tier2/artifactTools.js';
28
30
  export { MCP_CONNECTION_TOOLS, mcpToolCallTool, mcpToolsListTool, } from './tier2/mcpConnectionTools.js';
29
31
  export { CHAT_TOOLS, openConversationTool } from './tier2/chatTools.js';
@@ -27,10 +27,13 @@ export { CONTEXT_TOOLS, contextReadTool, contextDelegateTool, contextExpandReach
27
27
  // one grant lister across every rail (retires context_discover +
28
28
  // payment_list_grants). Always-available on the hosted agent — see CLAUDE_SDK_TOOLS.
29
29
  export { GRANTS_TOOLS, listGrantsTool } from './tier2/grantsTools.js';
30
+ export { TASK_TOOLS, listTasksTool } from './tier2/taskTools.js';
30
31
  // links (agreements with engagementKind 'link') — cross-org reach.
31
32
  ///1022: the rail is two tools; propose/claim/revoke ride the
32
33
  // agreement grammar.
33
34
  export { LINK_TOOLS, linkProposeTool, linkListTool, } from './tier2/linkTools.js';
35
+ // introduction tokens — a meeting somewhere else turned into a link here.
36
+ export { INTRODUCTION_TOOLS, introductionMintTool, introductionRedeemTool, introductionListTool, introductionRevokeTool, } from './tier2/introductionTools.js';
34
37
  // deliberate artifact recording (the model decides "record this").
35
38
  export { ARTIFACT_TOOLS, recordArtifactTool } from './tier2/artifactTools.js';
36
39
  export { MCP_CONNECTION_TOOLS, mcpToolCallTool, mcpToolsListTool, } from './tier2/mcpConnectionTools.js';