@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.
- package/dist/AgentHost.d.ts +23 -2
- package/dist/AgentHost.js +102 -2
- package/dist/adapters/McpAdapter.js +1 -1
- package/dist/agent/ClaudeSdkAgent.js +54 -4
- package/dist/agent/claudeSdkProtocol.d.ts +1 -1
- package/dist/agent/claudeSdkProtocol.js +2 -2
- package/dist/agent/claudeSdkTools.d.ts +2 -1
- package/dist/agent/claudeSdkTools.js +29 -58
- package/dist/index.d.ts +3 -2
- package/dist/index.js +2 -2
- package/dist/pricing/fleetTierByAgentId.gen.d.ts +1 -1
- package/dist/pricing/fleetTierByAgentId.gen.js +3 -8
- package/dist/runtime/AgentMachine.d.ts +28 -1
- package/dist/runtime/AgentMachine.js +55 -16
- package/dist/runtime/buildOutcome.d.ts +0 -8
- package/dist/runtime/buildOutcome.js +19 -5
- package/dist/runtime/kickoff.d.ts +23 -1
- package/dist/runtime/kickoff.js +45 -3
- package/dist/runtime/wakeOutcome.d.ts +27 -0
- package/dist/runtime/wakeOutcome.js +43 -0
- package/dist/server/ConnectionPool.js +11 -3
- package/dist/server/EventQueue.d.ts +23 -2
- package/dist/server/EventQueue.js +33 -3
- package/dist/server/agreements/AgreementService.d.ts +10 -1
- package/dist/server/agreements/AgreementService.js +9 -1
- package/dist/server/tools/index.d.ts +2 -0
- package/dist/server/tools/index.js +3 -0
- package/dist/server/tools/tier1/protocolRunner.js +33 -27
- package/dist/server/tools/tier1/protocolTools.js +2 -7
- package/dist/server/tools/tier2/introductionTools.d.ts +6 -0
- package/dist/server/tools/tier2/introductionTools.js +12 -0
- package/dist/server/tools/tier2/taskTools.d.ts +3 -0
- package/dist/server/tools/tier2/taskTools.js +6 -0
- package/dist/tools/recordReport.d.ts +5 -0
- package/dist/tools/recordReport.js +5 -0
- 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
|
-
/**
|
|
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.
|
|
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.
|
|
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
|
-
//
|
|
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.
|
|
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.
|
|
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
|
-
//
|
|
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
|
-
/**
|
|
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
|
-
|
|
345
|
-
|
|
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
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
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
|
package/dist/runtime/kickoff.js
CHANGED
|
@@ -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
|
-
|
|
169
|
-
|
|
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) =>
|
|
108
|
-
|
|
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
|
-
/**
|
|
29
|
-
|
|
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
|
-
/**
|
|
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
|
-
|
|
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';
|