@ziggs-ai/agent-sdk 0.10.0 → 0.10.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/CHANGELOG.md +23 -0
- package/dist/AgentHost.d.ts +50 -14
- package/dist/AgentHost.js +129 -7
- package/dist/agent/ClaudeSdkAgent.d.ts +23 -1
- package/dist/agent/ClaudeSdkAgent.js +145 -75
- package/dist/context/applyEffects.js +4 -3
- package/dist/index.d.ts +3 -2
- package/dist/index.js +3 -2
- package/dist/runtime/bootstrapCtx.js +6 -8
- package/dist/runtime/buildOutcome.d.ts +9 -1
- package/dist/runtime/buildOutcome.js +29 -6
- package/dist/runtime/taskIntake.js +6 -4
- package/dist/runtime/wakeWriteKeys.js +14 -21
- package/dist/server/ConnectionPool.d.ts +10 -52
- package/dist/server/ConnectionPool.js +26 -57
- package/dist/server/EventQueue.d.ts +6 -0
- package/dist/server/EventQueue.js +21 -6
- package/dist/server/SeenMessages.d.ts +7 -0
- package/dist/server/SeenMessages.js +15 -0
- package/dist/server/ZiggsEffectHandler.d.ts +1 -0
- package/dist/server/ZiggsEffectHandler.js +14 -7
- package/dist/server/inboxLoop.d.ts +17 -2
- package/dist/server/inboxLoop.js +92 -16
- package/dist/server/polls/PollKind.d.ts +72 -0
- package/dist/server/polls/PollKind.js +53 -0
- package/dist/server/polls/PollRunner.d.ts +62 -0
- package/dist/server/polls/PollRunner.js +138 -0
- package/dist/server/questMatch.d.ts +5 -0
- package/dist/server/questMatch.js +14 -0
- package/dist/server/runLauncher.d.ts +0 -6
- package/dist/server/runLauncher.js +54 -27
- package/dist/server/tools/tier1/protocolRunner.d.ts +3 -1
- package/dist/server/tools/tier1/protocolRunner.js +24 -28
- package/dist/server/tools/tier1/protocolTools.js +23 -13
- package/dist/server/tools/tier2/connectionTools.js +12 -1
- package/dist/tasks/protocolRegistry.d.ts +27 -0
- package/dist/tasks/protocolRegistry.js +36 -0
- package/dist/types.d.ts +0 -9
- package/package.json +2 -2
- package/dist/server/staleBacklogGate.d.ts +0 -57
- package/dist/server/staleBacklogGate.js +0 -100
|
@@ -1,17 +1,3 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* ConnectionPool — multi-agent fleet manager.
|
|
3
|
-
*
|
|
4
|
-
* Holds N `AgentHostOptions` configs but only constructs/wakes an
|
|
5
|
-
* `AgentHost` (one WS connection) when needed. Idle hosts are slept after
|
|
6
|
-
* `idleTimeoutMs`. At most `maxActive` hosts are connected at a time; LRU
|
|
7
|
-
* eviction drops the oldest if the cap is reached. With `startControl`, a
|
|
8
|
-
* separate "launcher" socket receives backend-driven wake hints so the pool
|
|
9
|
-
* spins up the right host on demand.
|
|
10
|
-
*
|
|
11
|
-
* This is how a single Node process serves a fleet of agents without
|
|
12
|
-
* holding 1000 open sockets — typical config: 1000+ registered, ~50 live.
|
|
13
|
-
*/
|
|
14
|
-
import { type InboxOperatorAgentEntry, type StartAgentOptions } from '@ziggs-ai/api-client';
|
|
15
1
|
import { AgentHost, type AgentHostOptions } from '../AgentHost.js';
|
|
16
2
|
interface ConnectionManagerMeta {
|
|
17
3
|
domain?: string;
|
|
@@ -24,44 +10,27 @@ interface QueryFilter {
|
|
|
24
10
|
expertise?: string[];
|
|
25
11
|
tags?: string[];
|
|
26
12
|
}
|
|
27
|
-
export interface PoolOptions {
|
|
28
|
-
maxActive?: number;
|
|
29
|
-
idleTimeoutMs?: number;
|
|
30
|
-
/**
|
|
31
|
-
* Gate on the operator-poll lazy start (ZIG-910): return false to leave an
|
|
32
|
-
* actionable agent's host down this pass. Explicit startAgent()s are not gated.
|
|
33
|
-
*/
|
|
34
|
-
shouldLazyStart?: (entry: InboxOperatorAgentEntry) => boolean;
|
|
35
|
-
}
|
|
36
13
|
export interface SendToOptions {
|
|
37
14
|
timeoutMs?: number;
|
|
38
15
|
}
|
|
39
|
-
export interface ControlOptions {
|
|
40
|
-
wsUrl?: string;
|
|
41
|
-
operatorKey?: string;
|
|
42
|
-
}
|
|
43
16
|
export declare class ConnectionPool {
|
|
44
17
|
private readonly manager;
|
|
45
18
|
private readonly configs;
|
|
46
19
|
private readonly responseListeners;
|
|
47
|
-
constructor({ maxActive, idleTimeoutMs, shouldLazyStart }?: PoolOptions);
|
|
48
20
|
/**
|
|
49
|
-
* Register N agent configs.
|
|
50
|
-
*
|
|
21
|
+
* Register N agent configs. Nothing connects until `connectAll()`; a config
|
|
22
|
+
* registered and never connected is an agent nobody hosts, which is the
|
|
23
|
+
* failure this pool exists to make impossible.
|
|
51
24
|
*/
|
|
52
25
|
register(configs: AgentHostOptions[], metaArr?: ConnectionManagerMeta[]): void;
|
|
53
|
-
/**
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
*/
|
|
58
|
-
startAgent(agentId: string, opts?: StartAgentOptions): Promise<AgentHost>;
|
|
59
|
-
/** Agents kept up explicitly (launcher `preStart`), not by demand. */
|
|
60
|
-
listPinned(): string[];
|
|
26
|
+
/** Connect every registered agent. Reports the ones that could not come up. */
|
|
27
|
+
connectAll(): Promise<void>;
|
|
28
|
+
/** The host for one agent, connecting it if it somehow is not up yet. */
|
|
29
|
+
startAgent(agentId: string): Promise<AgentHost>;
|
|
61
30
|
sleep(agentId: string): Promise<void>;
|
|
62
31
|
disconnectAll(): Promise<void>;
|
|
63
32
|
/**
|
|
64
|
-
*
|
|
33
|
+
* Deliver a message to the named agent and wait for its first outbound
|
|
65
34
|
* `send-message` effect — i.e. the agent's reply. Returns the reply text.
|
|
66
35
|
*
|
|
67
36
|
* The effectTap injected in `register` drains a per-agent listener queue;
|
|
@@ -70,28 +39,17 @@ export declare class ConnectionPool {
|
|
|
70
39
|
*/
|
|
71
40
|
requestFrom(agentId: string, text: string, metadata?: Record<string, unknown>, { timeoutMs }?: SendToOptions): Promise<string>;
|
|
72
41
|
/**
|
|
73
|
-
*
|
|
74
|
-
*
|
|
42
|
+
* Deliver one wire frame to the named agent. Returns once the host has
|
|
43
|
+
* finished processing.
|
|
75
44
|
*
|
|
76
45
|
* Note: does not return the agent's response. For request/response
|
|
77
46
|
* semantics use `requestFrom`.
|
|
78
47
|
*/
|
|
79
48
|
sendTo(agentId: string, text: string, metadata?: Record<string, unknown>, { timeoutMs }?: SendToOptions): Promise<null>;
|
|
80
|
-
/**
|
|
81
|
-
* Start the fleet lazy-start loop (delivery law, phase 4): the manager
|
|
82
|
-
* long-polls `GET /inbox/operator` with this operator key and starts the host
|
|
83
|
-
* for any owned agent whose slice has actionable content. Each started host
|
|
84
|
-
* then consumes via its own per-agent inbox loop — this is ordinary lazy
|
|
85
|
-
* process management, not a pushed signal. `wsUrl` is accepted for
|
|
86
|
-
* back-compat but unused (the poll is HTTP).
|
|
87
|
-
*/
|
|
88
|
-
startControl({ operatorKey }?: ControlOptions): void;
|
|
89
|
-
stopControl(): void;
|
|
90
49
|
query(filter: QueryFilter): string[];
|
|
91
50
|
list(): string[];
|
|
92
51
|
listActive(): string[];
|
|
93
52
|
get size(): number;
|
|
94
|
-
get maxActive(): number;
|
|
95
53
|
getMeta(id: string): ConnectionManagerMeta | undefined;
|
|
96
54
|
}
|
|
97
55
|
export {};
|
|
@@ -1,30 +1,30 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* ConnectionPool —
|
|
2
|
+
* ConnectionPool — the agents this launcher process hosts.
|
|
3
3
|
*
|
|
4
|
-
* Holds N `AgentHostOptions` configs
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
* separate "launcher" socket receives backend-driven wake hints so the pool
|
|
9
|
-
* spins up the right host on demand.
|
|
4
|
+
* Holds N `AgentHostOptions` configs and connects every one of them at boot
|
|
5
|
+
* (ZIG-1108). Each host long-polls its own inbox for the life of the process —
|
|
6
|
+
* there is no agent socket. There is no subset, no cap and no lifecycle policy:
|
|
7
|
+
* a write releasing that poll is the only signal anything needs.
|
|
10
8
|
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
9
|
+
* What this replaced: a launcher that registered 1000+ configs and connected
|
|
10
|
+
* ~50, deciding which by long-polling `GET /inbox/operator` for a fleet-wide
|
|
11
|
+
* sweep, sleeping quiet hosts on an idle timer, evicting past `maxActive` by
|
|
12
|
+
* LRU, and exempting operator-named agents with a `pinned` flag. Whether an
|
|
13
|
+
* agent was alive came down to a Terraform string, and nothing noticed when the
|
|
14
|
+
* answer was "none of them".
|
|
13
15
|
*/
|
|
14
16
|
import { ConnectionManager } from '@ziggs-ai/api-client';
|
|
15
17
|
import { AgentHost } from '../AgentHost.js';
|
|
16
18
|
import { runtimeLog } from '../shared/runtimeLog.js';
|
|
17
19
|
export class ConnectionPool {
|
|
18
|
-
manager;
|
|
20
|
+
manager = new ConnectionManager();
|
|
19
21
|
configs = new Map();
|
|
20
22
|
// Listeners enqueued by requestFrom — drained on outbound send-message effects.
|
|
21
23
|
responseListeners = new Map();
|
|
22
|
-
constructor({ maxActive = 50, idleTimeoutMs = 60_000, shouldLazyStart } = {}) {
|
|
23
|
-
this.manager = new ConnectionManager({ maxActive, idleTimeoutMs, shouldLazyStart });
|
|
24
|
-
}
|
|
25
24
|
/**
|
|
26
|
-
* Register N agent configs.
|
|
27
|
-
*
|
|
25
|
+
* Register N agent configs. Nothing connects until `connectAll()`; a config
|
|
26
|
+
* registered and never connected is an agent nobody hosts, which is the
|
|
27
|
+
* failure this pool exists to make impossible.
|
|
28
28
|
*/
|
|
29
29
|
register(configs, metaArr = []) {
|
|
30
30
|
const metaByAgentId = new Map();
|
|
@@ -46,14 +46,7 @@ export class ConnectionPool {
|
|
|
46
46
|
const pool = this;
|
|
47
47
|
const host = new AgentHost({
|
|
48
48
|
...cfg,
|
|
49
|
-
// Inbox activity and in-flight effects both reset the idle timer:
|
|
50
|
-
// previously only pool.sendTo() touched it, so a host woken by its
|
|
51
|
-
// own inbox loop was torn down every idleTimeoutMs (default 60s)
|
|
52
|
-
// regardless of the work it was doing — losing lane ctx, Claude
|
|
53
|
-
// session ids, and armed timers each time.
|
|
54
|
-
onActivity: () => pool.manager.touch(agentId),
|
|
55
49
|
effectTap(effect, response) {
|
|
56
|
-
pool.manager.touch(agentId);
|
|
57
50
|
if (effect.kind === 'send-message') {
|
|
58
51
|
const listeners = pool.responseListeners.get(agentId);
|
|
59
52
|
if (listeners?.length)
|
|
@@ -63,25 +56,21 @@ export class ConnectionPool {
|
|
|
63
56
|
},
|
|
64
57
|
});
|
|
65
58
|
await host.connectAsync();
|
|
66
|
-
runtimeLog.debug('ConnectionPool', `
|
|
59
|
+
runtimeLog.debug('ConnectionPool', `connected "${agentId}"`);
|
|
67
60
|
return host;
|
|
68
61
|
}, async (host) => {
|
|
69
62
|
host.disconnect();
|
|
70
|
-
runtimeLog.debug('ConnectionPool', `
|
|
63
|
+
runtimeLog.debug('ConnectionPool', `disconnected "${agentId}"`);
|
|
71
64
|
}, metaByAgentId.get(agentId));
|
|
72
65
|
}
|
|
73
66
|
}
|
|
74
|
-
/**
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
* named rather than ones work happened to reach.
|
|
78
|
-
*/
|
|
79
|
-
async startAgent(agentId, opts = {}) {
|
|
80
|
-
return await this.manager.startAgent(agentId, opts);
|
|
67
|
+
/** Connect every registered agent. Reports the ones that could not come up. */
|
|
68
|
+
async connectAll() {
|
|
69
|
+
return this.manager.connectAll();
|
|
81
70
|
}
|
|
82
|
-
/**
|
|
83
|
-
|
|
84
|
-
return this.manager.
|
|
71
|
+
/** The host for one agent, connecting it if it somehow is not up yet. */
|
|
72
|
+
async startAgent(agentId) {
|
|
73
|
+
return await this.manager.startAgent(agentId);
|
|
85
74
|
}
|
|
86
75
|
async sleep(agentId) {
|
|
87
76
|
return this.manager.sleep(agentId);
|
|
@@ -90,7 +79,7 @@ export class ConnectionPool {
|
|
|
90
79
|
return this.manager.sleepAll();
|
|
91
80
|
}
|
|
92
81
|
/**
|
|
93
|
-
*
|
|
82
|
+
* Deliver a message to the named agent and wait for its first outbound
|
|
94
83
|
* `send-message` effect — i.e. the agent's reply. Returns the reply text.
|
|
95
84
|
*
|
|
96
85
|
* The effectTap injected in `register` drains a per-agent listener queue;
|
|
@@ -139,8 +128,8 @@ export class ConnectionPool {
|
|
|
139
128
|
});
|
|
140
129
|
}
|
|
141
130
|
/**
|
|
142
|
-
*
|
|
143
|
-
*
|
|
131
|
+
* Deliver one wire frame to the named agent. Returns once the host has
|
|
132
|
+
* finished processing.
|
|
144
133
|
*
|
|
145
134
|
* Note: does not return the agent's response. For request/response
|
|
146
135
|
* semantics use `requestFrom`.
|
|
@@ -149,31 +138,11 @@ export class ConnectionPool {
|
|
|
149
138
|
const host = await this.startAgent(agentId);
|
|
150
139
|
const timeoutPromise = new Promise((_, reject) => setTimeout(() => reject(new Error(`sendTo timeout for "${agentId}" after ${timeoutMs}ms`)), timeoutMs));
|
|
151
140
|
await Promise.race([host.handleMessage(text, metadata), timeoutPromise]);
|
|
152
|
-
this.manager.touch(agentId);
|
|
153
141
|
return null;
|
|
154
142
|
}
|
|
155
|
-
/**
|
|
156
|
-
* Start the fleet lazy-start loop (delivery law, phase 4): the manager
|
|
157
|
-
* long-polls `GET /inbox/operator` with this operator key and starts the host
|
|
158
|
-
* for any owned agent whose slice has actionable content. Each started host
|
|
159
|
-
* then consumes via its own per-agent inbox loop — this is ordinary lazy
|
|
160
|
-
* process management, not a pushed signal. `wsUrl` is accepted for
|
|
161
|
-
* back-compat but unused (the poll is HTTP).
|
|
162
|
-
*/
|
|
163
|
-
startControl({ operatorKey } = {}) {
|
|
164
|
-
if (!operatorKey) {
|
|
165
|
-
runtimeLog.warn('ConnectionPool', 'startControl: operatorKey is required — skipping');
|
|
166
|
-
return;
|
|
167
|
-
}
|
|
168
|
-
this.manager.start(operatorKey);
|
|
169
|
-
}
|
|
170
|
-
stopControl() {
|
|
171
|
-
void this.manager.stop();
|
|
172
|
-
}
|
|
173
143
|
query(filter) { return this.manager.query(filter); }
|
|
174
144
|
list() { return this.manager.list(); }
|
|
175
145
|
listActive() { return this.manager.listActive(); }
|
|
176
146
|
get size() { return this.manager.size; }
|
|
177
|
-
get maxActive() { return this.manager.maxActive; }
|
|
178
147
|
getMeta(id) { return this.manager.getMeta(id); }
|
|
179
148
|
}
|
|
@@ -7,6 +7,12 @@ export interface EventQueueOptions {
|
|
|
7
7
|
* batch dispatch. The runtime sees at most one in-flight processEvent call
|
|
8
8
|
* per laneKey at a time; multiple events arriving while busy are merged
|
|
9
9
|
* into a single { type: 'batch', events: [...] } frame on the next pass.
|
|
10
|
+
*
|
|
11
|
+
* ZIG-1191: `enqueue` REJECTS when the dispatch failed. It used to log the error
|
|
12
|
+
* and resolve, which made every caller — including the inbox loop, whose next
|
|
13
|
+
* statement is the ack — unable to tell a handled event from a lost one. A
|
|
14
|
+
* caller that has nothing to do with a failure should say so with `.catch()`;
|
|
15
|
+
* the one holding a delivery must not have that decision made for it.
|
|
10
16
|
*/
|
|
11
17
|
export declare class EventQueue {
|
|
12
18
|
private _processEvent;
|
|
@@ -4,6 +4,12 @@ import { runtimeLog } from '../shared/runtimeLog.js';
|
|
|
4
4
|
* batch dispatch. The runtime sees at most one in-flight processEvent call
|
|
5
5
|
* per laneKey at a time; multiple events arriving while busy are merged
|
|
6
6
|
* into a single { type: 'batch', events: [...] } frame on the next pass.
|
|
7
|
+
*
|
|
8
|
+
* ZIG-1191: `enqueue` REJECTS when the dispatch failed. It used to log the error
|
|
9
|
+
* and resolve, which made every caller — including the inbox loop, whose next
|
|
10
|
+
* statement is the ack — unable to tell a handled event from a lost one. A
|
|
11
|
+
* caller that has nothing to do with a failure should say so with `.catch()`;
|
|
12
|
+
* the one holding a delivery must not have that decision made for it.
|
|
7
13
|
*/
|
|
8
14
|
export class EventQueue {
|
|
9
15
|
_processEvent;
|
|
@@ -28,9 +34,12 @@ export class EventQueue {
|
|
|
28
34
|
state = { events: [], processing: false };
|
|
29
35
|
this._state.set(laneKey, state);
|
|
30
36
|
}
|
|
31
|
-
return new Promise((resolve) => {
|
|
37
|
+
return new Promise((resolve, reject) => {
|
|
32
38
|
this._metrics.enqueued += 1;
|
|
33
|
-
state.events.push({
|
|
39
|
+
state.events.push({
|
|
40
|
+
event,
|
|
41
|
+
settle: (err) => (err === undefined ? resolve() : reject(err)),
|
|
42
|
+
});
|
|
34
43
|
this._trimQueueIfNeeded(state, laneKey);
|
|
35
44
|
this._processIfNeeded(laneKey);
|
|
36
45
|
});
|
|
@@ -49,21 +58,25 @@ export class EventQueue {
|
|
|
49
58
|
while (state.events.length > 0) {
|
|
50
59
|
const batch = state.events.splice(0, state.events.length);
|
|
51
60
|
const events = batch.map(e => e.event);
|
|
52
|
-
const
|
|
61
|
+
const settlers = batch.map(e => e.settle);
|
|
53
62
|
const coalesced = events.length === 1
|
|
54
63
|
? events[0]
|
|
55
64
|
: { type: 'batch', events };
|
|
65
|
+
// Coalescing makes the batch the unit of success: the lane ran one tick
|
|
66
|
+
// for all of it, so a failure belongs to every event that was merged in.
|
|
67
|
+
let failure;
|
|
56
68
|
try {
|
|
57
69
|
await this._processEvent(coalesced, laneKey);
|
|
58
70
|
}
|
|
59
71
|
catch (error) {
|
|
72
|
+
failure = error ?? new Error('processEvent failed');
|
|
60
73
|
this._metrics.errors += 1;
|
|
61
74
|
runtimeLog.error('EventQueue', `processEvent error for laneKey=${laneKey}: ${error?.message || error}`);
|
|
62
75
|
}
|
|
63
76
|
this._metrics.processedBatches += 1;
|
|
64
77
|
this._metrics.processedEvents += events.length;
|
|
65
|
-
for (const
|
|
66
|
-
|
|
78
|
+
for (const settle of settlers)
|
|
79
|
+
settle(failure);
|
|
67
80
|
}
|
|
68
81
|
state.processing = false;
|
|
69
82
|
}
|
|
@@ -71,7 +84,9 @@ export class EventQueue {
|
|
|
71
84
|
while (state.events.length > this._maxQueueLengthPerLane) {
|
|
72
85
|
const dropped = state.events.shift();
|
|
73
86
|
this._metrics.dropped += 1;
|
|
74
|
-
dropped
|
|
87
|
+
// A dropped event was never handled, so its caller hears about it — the
|
|
88
|
+
// inbox loop holds the ack instead of acking work that fell off the queue.
|
|
89
|
+
dropped?.settle(new Error(`EventQueue dropped an event on laneKey=${laneKey} (queue over ${this._maxQueueLengthPerLane})`));
|
|
75
90
|
runtimeLog.warn('EventQueue', `dropped oldest event laneKey=${laneKey} max=${this._maxQueueLengthPerLane}`);
|
|
76
91
|
}
|
|
77
92
|
}
|
|
@@ -10,4 +10,11 @@ export declare class SeenMessages {
|
|
|
10
10
|
constructor(maxSize?: number);
|
|
11
11
|
has(messageId: string | null | undefined): boolean;
|
|
12
12
|
mark(messageId: string | null | undefined): void;
|
|
13
|
+
/**
|
|
14
|
+
* Forget a mark, so the next delivery of the same id is processed rather than
|
|
15
|
+
* skipped. ZIG-1191: marking happens BEFORE the work, to hold the door against
|
|
16
|
+
* a concurrent duplicate — which means a failure has to take the mark back, or
|
|
17
|
+
* the redelivery that a held ack buys is silently dropped as a duplicate.
|
|
18
|
+
*/
|
|
19
|
+
unmark(messageId: string | null | undefined): void;
|
|
13
20
|
}
|
|
@@ -28,4 +28,19 @@ export class SeenMessages {
|
|
|
28
28
|
this.seen.delete(oldest);
|
|
29
29
|
}
|
|
30
30
|
}
|
|
31
|
+
/**
|
|
32
|
+
* Forget a mark, so the next delivery of the same id is processed rather than
|
|
33
|
+
* skipped. ZIG-1191: marking happens BEFORE the work, to hold the door against
|
|
34
|
+
* a concurrent duplicate — which means a failure has to take the mark back, or
|
|
35
|
+
* the redelivery that a held ack buys is silently dropped as a duplicate.
|
|
36
|
+
*/
|
|
37
|
+
unmark(messageId) {
|
|
38
|
+
if (!messageId || typeof messageId !== 'string')
|
|
39
|
+
return;
|
|
40
|
+
if (!this.seen.delete(messageId))
|
|
41
|
+
return;
|
|
42
|
+
const at = this.order.indexOf(messageId);
|
|
43
|
+
if (at !== -1)
|
|
44
|
+
this.order.splice(at, 1);
|
|
45
|
+
}
|
|
31
46
|
}
|
|
@@ -67,6 +67,7 @@ export declare function executeToolWithSideEffects(deps: Pick<ZiggsEffectDeps, '
|
|
|
67
67
|
}>, roster: {
|
|
68
68
|
agents: unknown[];
|
|
69
69
|
users: unknown[];
|
|
70
|
+
agreements?: unknown[];
|
|
70
71
|
}): Promise<ToolCallResult>;
|
|
71
72
|
/**
|
|
72
73
|
* Build a single shared EffectHandler. The handler is stateful — it caches
|
|
@@ -109,7 +109,10 @@ export async function executeToolWithSideEffects(deps, eff, effect, roster) {
|
|
|
109
109
|
const toolT0 = Date.now();
|
|
110
110
|
const args = effect.args || {};
|
|
111
111
|
const argsPreview = previewToolArgs(args);
|
|
112
|
-
|
|
112
|
+
// ZIG-1120 #6 — breadcrumbs are best-effort. Awaiting them bracketed every
|
|
113
|
+
// tool call; a slow/wedged artifacts backend stalled the whole wake. Match
|
|
114
|
+
// ClaudeSdkAgent thought writes (fire-and-forget).
|
|
115
|
+
void emitToolOperationArtifact(deps.artifactsClient, effect.sessionId, 'started', {
|
|
113
116
|
state: 'started',
|
|
114
117
|
tool: effect.name,
|
|
115
118
|
...(argsPreview ? { args: argsPreview } : {}),
|
|
@@ -142,6 +145,9 @@ export async function executeToolWithSideEffects(deps, eff, effect, roster) {
|
|
|
142
145
|
eff,
|
|
143
146
|
agents: roster.agents,
|
|
144
147
|
users: roster.users,
|
|
148
|
+
// ZIG-1120 #8 — session snapshot already has agreements; protocolRunner
|
|
149
|
+
// used to re-fetch them over HTTP on every spawn/subcontract.
|
|
150
|
+
agreements: roster.agreements ?? [],
|
|
145
151
|
senderId: effect.senderId ?? lastUser?.userId ?? lastUser?.id,
|
|
146
152
|
senderType: effect.senderType ?? (lastUser ? 'USER' : undefined),
|
|
147
153
|
onBehalfOfAgreementId: effect.onBehalfOfAgreementId,
|
|
@@ -153,7 +159,7 @@ export async function executeToolWithSideEffects(deps, eff, effect, roster) {
|
|
|
153
159
|
// One clip per tool call: this is a full JSON.stringify of the untruncated
|
|
154
160
|
// result, and the artifact body and the idempotency key want the same string.
|
|
155
161
|
const clipped = clipForOperationArtifact(result);
|
|
156
|
-
|
|
162
|
+
void emitToolOperationArtifact(deps.artifactsClient, effect.sessionId, 'completed', {
|
|
157
163
|
state: 'completed',
|
|
158
164
|
tool: effect.name,
|
|
159
165
|
result: clipped,
|
|
@@ -171,7 +177,7 @@ export async function executeToolWithSideEffects(deps, eff, effect, roster) {
|
|
|
171
177
|
catch (error) {
|
|
172
178
|
const errMsg = error?.message ?? String(error);
|
|
173
179
|
runtimeLog.warn('ZiggsEffectHandler', `tool-call "${effect.name}" failed: ${errMsg}`);
|
|
174
|
-
|
|
180
|
+
void emitToolOperationArtifact(deps.artifactsClient, effect.sessionId, 'error', {
|
|
175
181
|
state: 'failed',
|
|
176
182
|
tool: effect.name,
|
|
177
183
|
error: errMsg,
|
|
@@ -277,14 +283,15 @@ export function createZiggsEffectHandler(deps) {
|
|
|
277
283
|
return { text, usage };
|
|
278
284
|
}
|
|
279
285
|
case 'tool-call': {
|
|
280
|
-
// Roster
|
|
281
|
-
// cognitions land here — the FSM via runTurn's
|
|
282
|
-
// Agent SDK via the toolManagerToSdkMcp bridge —
|
|
283
|
-
// emitted exactly once, in one place (ZIG-775).
|
|
286
|
+
// Roster + agreements come from the cached snapshot (populated by
|
|
287
|
+
// read-context). Both cognitions land here — the FSM via runTurn's
|
|
288
|
+
// executeTool, the Claude Agent SDK via the toolManagerToSdkMcp bridge —
|
|
289
|
+
// so tool side effects are emitted exactly once, in one place (ZIG-775).
|
|
284
290
|
const cached = cacheGet(effect.sessionId);
|
|
285
291
|
return executeToolWithSideEffects(deps, handler, effect, {
|
|
286
292
|
agents: cached?.agents || [],
|
|
287
293
|
users: cached?.users || [],
|
|
294
|
+
agreements: cached?.agreements || [],
|
|
288
295
|
});
|
|
289
296
|
}
|
|
290
297
|
case 'send-message': {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type InboxTaskRef, type Creds } from '@ziggs-ai/api-client';
|
|
1
|
+
import { type InboxQuestRef, type InboxTaskRef, type Creds } from '@ziggs-ai/api-client';
|
|
2
2
|
import { SeenMessages } from './SeenMessages.js';
|
|
3
3
|
/**
|
|
4
4
|
* Per-agent inbox long-poll loop (delivery law, phase 4). This is the ONLY way
|
|
@@ -26,6 +26,12 @@ export interface InboxLoopDeps {
|
|
|
26
26
|
* for it (chat-less agreement, self-assigned task).
|
|
27
27
|
*/
|
|
28
28
|
onTask?: (task: InboxTaskRef) => Promise<void>;
|
|
29
|
+
/**
|
|
30
|
+
* A marketplace quest doorbell (ZIG-1185). The host MUST triage with a plain
|
|
31
|
+
* string compare and return without an LLM wake on miss — otherwise every
|
|
32
|
+
* public quest burns a full Claude turn per published supplier.
|
|
33
|
+
*/
|
|
34
|
+
onQuest?: (quest: InboxQuestRef) => Promise<void>;
|
|
29
35
|
label?: string;
|
|
30
36
|
}
|
|
31
37
|
export interface InboxPassResult {
|
|
@@ -35,10 +41,17 @@ export interface InboxPassResult {
|
|
|
35
41
|
resourceEvents: number;
|
|
36
42
|
/** Assigned open tasks delivered this pass (ZIG-973). */
|
|
37
43
|
tasksDelivered: number;
|
|
44
|
+
/** Quest refs triaged this pass (ZIG-1185) — includes misses that only ack. */
|
|
45
|
+
questsTriaged: number;
|
|
38
46
|
/** True when the watermark advanced this pass. */
|
|
39
47
|
acked: boolean;
|
|
40
48
|
/** True when there was more mail than one envelope carries. */
|
|
41
49
|
truncated: boolean;
|
|
50
|
+
/**
|
|
51
|
+
* Handovers into the host that failed this pass (ZIG-1191). Non-zero means the
|
|
52
|
+
* watermark was deliberately held, so the same window arrives again.
|
|
53
|
+
*/
|
|
54
|
+
failures: number;
|
|
42
55
|
}
|
|
43
56
|
/**
|
|
44
57
|
* One long-poll pass: hold up to `waitSeconds` for new inbox content, then read
|
|
@@ -46,7 +59,9 @@ export interface InboxPassResult {
|
|
|
46
59
|
* Reused each iteration of {@link runInboxLoop}; also usable as a single catch-up
|
|
47
60
|
* read (`waitSeconds: 0`) at bootstrap.
|
|
48
61
|
*/
|
|
49
|
-
export declare function pollInboxOnce(deps: InboxLoopDeps, waitSeconds?: number
|
|
62
|
+
export declare function pollInboxOnce(deps: InboxLoopDeps, waitSeconds?: number, opts?: {
|
|
63
|
+
forceAck?: boolean;
|
|
64
|
+
}): Promise<InboxPassResult>;
|
|
50
65
|
export interface InboxLoopHandle {
|
|
51
66
|
/** Stop the loop after the current pass; resolves when it has fully exited. */
|
|
52
67
|
stop(): Promise<void>;
|