@ziggs-ai/agent-sdk 0.2.1 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/AgentHost.d.ts +21 -20
- package/dist/AgentHost.js +60 -136
- package/dist/adapters/McpAdapter.js +14 -13
- package/dist/agent/Agent.js +4 -0
- package/dist/agent/ClaudeSdkAgent.d.ts +5 -0
- package/dist/agent/ClaudeSdkAgent.js +25 -6
- package/dist/agent/claudeCognition.d.ts +7 -0
- package/dist/agent/claudeCognition.js +38 -7
- package/dist/agent/claudeSdkProtocol.d.ts +8 -7
- package/dist/agent/claudeSdkProtocol.js +8 -7
- package/dist/agent/claudeSdkTools.d.ts +4 -2
- package/dist/agent/claudeSdkTools.js +26 -6
- package/dist/agent/toolManagerToSdkMcp.js +1 -0
- package/dist/index.d.ts +5 -9
- package/dist/index.js +12 -6
- package/dist/ingress/normalizeIncoming.d.ts +17 -3
- package/dist/ingress/normalizeIncoming.js +11 -26
- package/dist/runtime/AgentMachine.d.ts +5 -2
- package/dist/runtime/AgentMachine.js +8 -2
- package/dist/runtime/runMechanical.d.ts +3 -1
- package/dist/runtime/runMechanical.js +2 -2
- package/dist/runtime/runTurn.d.ts +3 -1
- package/dist/runtime/runTurn.js +25 -7
- package/dist/runtime/wakeWriteKeys.d.ts +25 -0
- package/dist/runtime/wakeWriteKeys.js +134 -0
- package/dist/server/ConnectionPool.d.ts +7 -4
- package/dist/server/ConnectionPool.js +11 -14
- package/dist/server/ZiggsEffectHandler.js +17 -12
- package/dist/server/inboxLoop.d.ts +50 -0
- package/dist/server/{InboxCatchUp.js → inboxLoop.js} +63 -52
- package/dist/server/runLauncher.js +1 -1
- package/dist/server/tasks/TaskService.d.ts +2 -1
- package/dist/server/tasks/TaskService.js +5 -2
- package/dist/server/tools/index.d.ts +7 -3
- package/dist/server/tools/index.js +13 -3
- package/dist/server/tools/tier1/protocolRunner.d.ts +2 -1
- package/dist/server/tools/tier1/protocolRunner.js +43 -9
- package/dist/server/tools/tier1/protocolTools.js +1 -1
- package/dist/server/tools/tier2/artifactTools.d.ts +3 -0
- package/dist/server/tools/tier2/artifactTools.js +82 -0
- package/dist/server/tools/tier2/chatTools.d.ts +3 -0
- package/dist/server/tools/tier2/chatTools.js +37 -0
- package/dist/server/tools/tier2/connectionTools.d.ts +1 -0
- package/dist/server/tools/tier2/connectionTools.js +52 -5
- package/dist/server/tools/tier2/contextTools.d.ts +4 -2
- package/dist/server/tools/tier2/contextTools.js +105 -12
- package/dist/server/tools/tier2/grantsTools.d.ts +3 -0
- package/dist/server/tools/tier2/grantsTools.js +81 -0
- package/dist/server/tools/tier2/linkTools.d.ts +7 -0
- package/dist/server/tools/tier2/linkTools.js +156 -0
- package/dist/server/tools/tier2/paymentTools.d.ts +0 -1
- package/dist/server/tools/tier2/paymentTools.js +9 -13
- package/dist/types.d.ts +40 -0
- package/package.json +2 -2
- package/dist/server/InboxCatchUp.d.ts +0 -24
- package/dist/server/ziggsconnect/ZiggsConnectClient.d.ts +0 -35
- package/dist/server/ziggsconnect/ZiggsConnectClient.js +0 -95
- package/dist/server/ziggscontext/ZiggsContextClient.d.ts +0 -53
- package/dist/server/ziggscontext/ZiggsContextClient.js +0 -124
- package/dist/server/ziggspay/ZiggsPayClient.d.ts +0 -82
- package/dist/server/ziggspay/ZiggsPayClient.js +0 -203
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { WakeWriteKeys } from '../types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Build the per-wake key derivation. Call once per tick with the lane and the
|
|
4
|
+
* raw triggering event, then thread the instance to every write of the wake.
|
|
5
|
+
*
|
|
6
|
+
* Key shapes (all well under the backend's 128-char cap):
|
|
7
|
+
* - artifacts: `wk_<wake-hash>_a<ordinal>` — ordinal increments per
|
|
8
|
+
* artifact write, so a wake writing several breadcrumbs keys each one.
|
|
9
|
+
* - task transitions: `wk_<wake-hash>_t_<hash(taskId, state)>` — derived from
|
|
10
|
+
* the transition itself, so it dedups regardless of the order in which the
|
|
11
|
+
* replayed wake happens to re-issue its tool calls.
|
|
12
|
+
* - chat messages: `sdk_idem_wk_<wake-hash>_m<ordinal>` — used directly as
|
|
13
|
+
* the messageId (the backend dedups on (chatId, messageId), ZIG-718), drawn
|
|
14
|
+
* from the same ordinal stream as artifacts so one counter keys the wake's
|
|
15
|
+
* whole write sequence. The prefix matches the tool-layer convention for
|
|
16
|
+
* deterministic ids (`sdk_idem_<llm-key>`).
|
|
17
|
+
* - task spawns: `wk_<wake-hash>_s_<hash(agreement, description, …)>` —
|
|
18
|
+
* derived from what the spawn writes, so it dedups regardless of re-issue
|
|
19
|
+
* order while still distinguishing multiple spawns in one wake.
|
|
20
|
+
* - agreement create: `wk_<wake-hash>_ap_<hash(…)>` (propose) and
|
|
21
|
+
* `wk_<wake-hash>_asc_<hash(…)>` (subcontract) — same content-derived scheme
|
|
22
|
+
* as task spawns; the backend dedups on (creator, key) so a replayed
|
|
23
|
+
* propose/subcontract resolves to the original agreement.
|
|
24
|
+
*/
|
|
25
|
+
export declare function createWakeWriteKeys(laneKey: string, event: unknown): WakeWriteKeys;
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { getBatchEvents } from '../context/batch.js';
|
|
3
|
+
/**
|
|
4
|
+
* Deterministic idempotency keys for hosted-agent writes (delivery law, item 2).
|
|
5
|
+
*
|
|
6
|
+
* A crash between acting and acking redelivers the SAME wake event; the agent
|
|
7
|
+
* re-thinks and re-writes. The backend drops duplicate artifact writes and
|
|
8
|
+
* no-ops duplicate task transitions — but only when the replayed wake derives
|
|
9
|
+
* the SAME key. So keys are a pure function of durable wake inputs (laneKey +
|
|
10
|
+
* the triggering event's delivery identity + a per-wake write ordinal). They
|
|
11
|
+
* are never LLM-supplied and never random: asking the model to invent a key
|
|
12
|
+
* makes replay-stability a matter of sampling temperature.
|
|
13
|
+
*
|
|
14
|
+
* One instance is created per wake (tick) and threaded to every write the wake
|
|
15
|
+
* performs — a second instance created for the same (laneKey, event), as a
|
|
16
|
+
* crash-replay does, reproduces the same key sequence.
|
|
17
|
+
*/
|
|
18
|
+
/**
|
|
19
|
+
* Durable identity of the event that triggered this wake, or null when it has
|
|
20
|
+
* none. Null means the wake is not a redelivery candidate (timer and other
|
|
21
|
+
* host-synthesized frames fire fresh each time), so its writes carry no key —
|
|
22
|
+
* a derived-anyway key could only collide across genuinely distinct wakes and
|
|
23
|
+
* silently swallow their writes, which is worse than the duplicate it prevents.
|
|
24
|
+
*/
|
|
25
|
+
function eventIdentity(event) {
|
|
26
|
+
if (!event || typeof event !== 'object')
|
|
27
|
+
return null;
|
|
28
|
+
const e = event;
|
|
29
|
+
if (e.type === 'batch') {
|
|
30
|
+
// EventQueue coalesces queued events into one frame. Identify the batch by
|
|
31
|
+
// its identifiable members (sorted — arrival order is not replay-stable).
|
|
32
|
+
// Members without identity are ignored rather than nulling the whole batch:
|
|
33
|
+
// an identified member can only recur via redelivery, which is exactly when
|
|
34
|
+
// matching keys are wanted.
|
|
35
|
+
const parts = getBatchEvents(event)
|
|
36
|
+
.map(eventIdentity)
|
|
37
|
+
.filter((p) => p !== null)
|
|
38
|
+
.sort();
|
|
39
|
+
return parts.length > 0 ? parts.join('|') : null;
|
|
40
|
+
}
|
|
41
|
+
// Per-delivery id stamped by the backend on chat rows — present on live
|
|
42
|
+
// pushes and on catch-up redelivery alike, and the most precise identity.
|
|
43
|
+
if (typeof e.messageId === 'string' && e.messageId)
|
|
44
|
+
return `msg:${e.messageId}`;
|
|
45
|
+
if (e.type === 'task_result') {
|
|
46
|
+
// Host-synthesized task frames (bootstrap of already-assigned tasks) carry
|
|
47
|
+
// no messageId; the task snapshot itself is the identity. Re-bootstrapping
|
|
48
|
+
// the same task in the same state IS re-processing, so the collision this
|
|
49
|
+
// produces is the dedup we want.
|
|
50
|
+
const result = (e.result ?? {});
|
|
51
|
+
const taskId = typeof result.taskId === 'string' ? result.taskId : null;
|
|
52
|
+
if (!taskId)
|
|
53
|
+
return null;
|
|
54
|
+
const state = typeof result.state === 'string' ? result.state : '';
|
|
55
|
+
const operation = typeof result._operation === 'string' ? result._operation : '';
|
|
56
|
+
return `task:${taskId}:${state}:${operation}`;
|
|
57
|
+
}
|
|
58
|
+
if (e.type === 'agreement_lifecycle') {
|
|
59
|
+
const agreementId = typeof e.agreementId === 'string' ? e.agreementId : null;
|
|
60
|
+
if (!agreementId)
|
|
61
|
+
return null;
|
|
62
|
+
return `agr:${agreementId}:${typeof e.operation === 'string' ? e.operation : ''}`;
|
|
63
|
+
}
|
|
64
|
+
// state-timeout, resource_changed, and anything else: no per-delivery id on
|
|
65
|
+
// the wire, and never redelivered by catch-up — keyless by design.
|
|
66
|
+
return null;
|
|
67
|
+
}
|
|
68
|
+
function sha(input) {
|
|
69
|
+
return createHash('sha256').update(input).digest('hex');
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Build the per-wake key derivation. Call once per tick with the lane and the
|
|
73
|
+
* raw triggering event, then thread the instance to every write of the wake.
|
|
74
|
+
*
|
|
75
|
+
* Key shapes (all well under the backend's 128-char cap):
|
|
76
|
+
* - artifacts: `wk_<wake-hash>_a<ordinal>` — ordinal increments per
|
|
77
|
+
* artifact write, so a wake writing several breadcrumbs keys each one.
|
|
78
|
+
* - task transitions: `wk_<wake-hash>_t_<hash(taskId, state)>` — derived from
|
|
79
|
+
* the transition itself, so it dedups regardless of the order in which the
|
|
80
|
+
* replayed wake happens to re-issue its tool calls.
|
|
81
|
+
* - chat messages: `sdk_idem_wk_<wake-hash>_m<ordinal>` — used directly as
|
|
82
|
+
* the messageId (the backend dedups on (chatId, messageId), ZIG-718), drawn
|
|
83
|
+
* from the same ordinal stream as artifacts so one counter keys the wake's
|
|
84
|
+
* whole write sequence. The prefix matches the tool-layer convention for
|
|
85
|
+
* deterministic ids (`sdk_idem_<llm-key>`).
|
|
86
|
+
* - task spawns: `wk_<wake-hash>_s_<hash(agreement, description, …)>` —
|
|
87
|
+
* derived from what the spawn writes, so it dedups regardless of re-issue
|
|
88
|
+
* order while still distinguishing multiple spawns in one wake.
|
|
89
|
+
* - agreement create: `wk_<wake-hash>_ap_<hash(…)>` (propose) and
|
|
90
|
+
* `wk_<wake-hash>_asc_<hash(…)>` (subcontract) — same content-derived scheme
|
|
91
|
+
* as task spawns; the backend dedups on (creator, key) so a replayed
|
|
92
|
+
* propose/subcontract resolves to the original agreement.
|
|
93
|
+
*/
|
|
94
|
+
export function createWakeWriteKeys(laneKey, event) {
|
|
95
|
+
const identity = eventIdentity(event);
|
|
96
|
+
const wakeHash = identity === null ? null : sha(`${laneKey}\n${identity}`).slice(0, 40);
|
|
97
|
+
let ordinal = 0;
|
|
98
|
+
return {
|
|
99
|
+
wakeKey: wakeHash === null ? null : `wk_${wakeHash}`,
|
|
100
|
+
artifactKey() {
|
|
101
|
+
if (wakeHash === null)
|
|
102
|
+
return undefined;
|
|
103
|
+
return `wk_${wakeHash}_a${ordinal++}`;
|
|
104
|
+
},
|
|
105
|
+
taskStateKey(taskId, state) {
|
|
106
|
+
if (wakeHash === null)
|
|
107
|
+
return undefined;
|
|
108
|
+
return `wk_${wakeHash}_t_${sha(`${taskId}\n${state}`).slice(0, 16)}`;
|
|
109
|
+
},
|
|
110
|
+
messageKey() {
|
|
111
|
+
if (wakeHash === null)
|
|
112
|
+
return undefined;
|
|
113
|
+
return `sdk_idem_wk_${wakeHash}_m${ordinal++}`;
|
|
114
|
+
},
|
|
115
|
+
taskSpawnKey(fields) {
|
|
116
|
+
if (wakeHash === null)
|
|
117
|
+
return undefined;
|
|
118
|
+
const spawnHash = sha(`${fields.agreementId}\n${fields.description}\n${fields.assigneeId ?? ''}\n${fields.parentTaskId ?? ''}`).slice(0, 16);
|
|
119
|
+
return `wk_${wakeHash}_s_${spawnHash}`;
|
|
120
|
+
},
|
|
121
|
+
agreementProposeKey(fields) {
|
|
122
|
+
if (wakeHash === null)
|
|
123
|
+
return undefined;
|
|
124
|
+
const proposeHash = sha(`${fields.proposedTo}\n${fields.providerId ?? ''}\n${fields.description}\n${fields.parentAgreementId ?? ''}`).slice(0, 16);
|
|
125
|
+
return `wk_${wakeHash}_ap_${proposeHash}`;
|
|
126
|
+
},
|
|
127
|
+
agreementSubcontractKey(fields) {
|
|
128
|
+
if (wakeHash === null)
|
|
129
|
+
return undefined;
|
|
130
|
+
const subHash = sha(`${fields.executorId}\n${fields.description}\n${fields.parentAgreementId}`).slice(0, 16);
|
|
131
|
+
return `wk_${wakeHash}_asc_${subHash}`;
|
|
132
|
+
},
|
|
133
|
+
};
|
|
134
|
+
}
|
|
@@ -52,11 +52,14 @@ export declare class ConnectionPool {
|
|
|
52
52
|
*/
|
|
53
53
|
sendTo(agentId: string, text: string, metadata?: Record<string, unknown>, { timeoutMs }?: SendToOptions): Promise<null>;
|
|
54
54
|
/**
|
|
55
|
-
*
|
|
56
|
-
*
|
|
57
|
-
*
|
|
55
|
+
* Start the fleet lazy-start loop (delivery law, phase 4): the manager
|
|
56
|
+
* long-polls `GET /inbox/operator` with this operator key and starts the host
|
|
57
|
+
* for any owned agent whose slice has actionable content. Each started host
|
|
58
|
+
* then consumes via its own per-agent inbox loop — this is ordinary lazy
|
|
59
|
+
* process management, not a pushed signal. `wsUrl` is accepted for
|
|
60
|
+
* back-compat but unused (the poll is HTTP).
|
|
58
61
|
*/
|
|
59
|
-
startControl({
|
|
62
|
+
startControl({ operatorKey }?: ControlOptions): void;
|
|
60
63
|
stopControl(): void;
|
|
61
64
|
query(filter: QueryFilter): string[];
|
|
62
65
|
list(): string[];
|
|
@@ -137,25 +137,22 @@ export class ConnectionPool {
|
|
|
137
137
|
return null;
|
|
138
138
|
}
|
|
139
139
|
/**
|
|
140
|
-
*
|
|
141
|
-
*
|
|
142
|
-
*
|
|
140
|
+
* Start the fleet lazy-start loop (delivery law, phase 4): the manager
|
|
141
|
+
* long-polls `GET /inbox/operator` with this operator key and starts the host
|
|
142
|
+
* for any owned agent whose slice has actionable content. Each started host
|
|
143
|
+
* then consumes via its own per-agent inbox loop — this is ordinary lazy
|
|
144
|
+
* process management, not a pushed signal. `wsUrl` is accepted for
|
|
145
|
+
* back-compat but unused (the poll is HTTP).
|
|
143
146
|
*/
|
|
144
|
-
startControl({
|
|
145
|
-
if (!
|
|
146
|
-
runtimeLog.warn('ConnectionPool', 'startControl:
|
|
147
|
+
startControl({ operatorKey } = {}) {
|
|
148
|
+
if (!operatorKey) {
|
|
149
|
+
runtimeLog.warn('ConnectionPool', 'startControl: operatorKey is required — skipping');
|
|
147
150
|
return;
|
|
148
151
|
}
|
|
149
|
-
|
|
150
|
-
// private field is the smallest way to add it later. (Cleaner fix lives
|
|
151
|
-
// in api-client: expose a `setControl` setter.)
|
|
152
|
-
this.manager['_controlOpts'] = { wsUrl, operatorKey };
|
|
153
|
-
this.manager.start();
|
|
152
|
+
this.manager.start(operatorKey);
|
|
154
153
|
}
|
|
155
154
|
stopControl() {
|
|
156
|
-
|
|
157
|
-
m['_controlHandle']?.close?.();
|
|
158
|
-
m['_controlHandle'] = null;
|
|
155
|
+
void this.manager.stop();
|
|
159
156
|
}
|
|
160
157
|
query(filter) { return this.manager.query(filter); }
|
|
161
158
|
list() { return this.manager.list(); }
|
|
@@ -69,7 +69,7 @@ function previewToolArgs(args) {
|
|
|
69
69
|
* Chat timeline: `Message.tsx` parses `entryType: artifact`, `content_type: operation` with
|
|
70
70
|
* `operation_*:{json}` where json includes `state` and `tool`.
|
|
71
71
|
*/
|
|
72
|
-
async function emitToolOperationArtifact(client, chatId, phase, payload) {
|
|
72
|
+
async function emitToolOperationArtifact(client, chatId, phase, payload, idempotencyKey) {
|
|
73
73
|
const prefix = phase === 'started' ? 'operation_started' : phase === 'completed' ? 'operation_completed' : 'operation_error';
|
|
74
74
|
const text = `${prefix}:${JSON.stringify(payload)}`;
|
|
75
75
|
try {
|
|
@@ -78,6 +78,7 @@ async function emitToolOperationArtifact(client, chatId, phase, payload) {
|
|
|
78
78
|
text,
|
|
79
79
|
content_type: 'operation',
|
|
80
80
|
visibility: 'agent-private',
|
|
81
|
+
idempotencyKey,
|
|
81
82
|
});
|
|
82
83
|
}
|
|
83
84
|
catch (e) {
|
|
@@ -108,7 +109,7 @@ export async function executeToolWithSideEffects(deps, eff, effect, roster) {
|
|
|
108
109
|
state: 'started',
|
|
109
110
|
tool: effect.name,
|
|
110
111
|
...(argsPreview ? { args: argsPreview } : {}),
|
|
111
|
-
});
|
|
112
|
+
}, effect.wakeKeys?.artifactKey());
|
|
112
113
|
try {
|
|
113
114
|
const lastUser = roster.users.find((u) => u.userId || u.id);
|
|
114
115
|
// Accumulate token usage from any nested LLM call the tool makes (llmTool),
|
|
@@ -141,6 +142,7 @@ export async function executeToolWithSideEffects(deps, eff, effect, roster) {
|
|
|
141
142
|
senderType: effect.senderType ?? (lastUser ? 'USER' : undefined),
|
|
142
143
|
onBehalfOfAgreementId: effect.onBehalfOfAgreementId,
|
|
143
144
|
memory: effect.memory,
|
|
145
|
+
wakeKeys: effect.wakeKeys,
|
|
144
146
|
reportUsage,
|
|
145
147
|
turnScratch: {},
|
|
146
148
|
});
|
|
@@ -148,7 +150,7 @@ export async function executeToolWithSideEffects(deps, eff, effect, roster) {
|
|
|
148
150
|
state: 'completed',
|
|
149
151
|
tool: effect.name,
|
|
150
152
|
result: clipForOperationArtifact(result),
|
|
151
|
-
});
|
|
153
|
+
}, effect.wakeKeys?.artifactKey());
|
|
152
154
|
sendTelemetry(deps.telemetryClient, buildToolIngestPayload({
|
|
153
155
|
sessionId: effect.sessionId,
|
|
154
156
|
tool: effect.name,
|
|
@@ -166,7 +168,7 @@ export async function executeToolWithSideEffects(deps, eff, effect, roster) {
|
|
|
166
168
|
state: 'failed',
|
|
167
169
|
tool: effect.name,
|
|
168
170
|
error: errMsg,
|
|
169
|
-
});
|
|
171
|
+
}, effect.wakeKeys?.artifactKey());
|
|
170
172
|
sendTelemetry(deps.telemetryClient, buildToolIngestPayload({
|
|
171
173
|
sessionId: effect.sessionId,
|
|
172
174
|
tool: effect.name,
|
|
@@ -242,13 +244,13 @@ export function createZiggsEffectHandler(deps) {
|
|
|
242
244
|
let usage;
|
|
243
245
|
if (deps.llm.chatMessagesStream) {
|
|
244
246
|
const streamed = await deps.llm.chatMessagesStream(messages, (chunk) => {
|
|
245
|
-
// Live-
|
|
246
|
-
//
|
|
247
|
-
//
|
|
248
|
-
//
|
|
249
|
-
//
|
|
250
|
-
//
|
|
251
|
-
//
|
|
247
|
+
// Live-token display is best-effort and optional. Hosted agents wire
|
|
248
|
+
// no chunkSender (delivery law, phase 4: no agent socket, no live
|
|
249
|
+
// chunk transport), so partials are simply dropped — `text`
|
|
250
|
+
// accumulates independently and the final send-message (below, in
|
|
251
|
+
// runTurn) carries the full reply as one whole HTTP message. Any
|
|
252
|
+
// chunk-send failure is swallowed so a display hiccup never aborts
|
|
253
|
+
// the stream and loses the reply.
|
|
252
254
|
try {
|
|
253
255
|
if (chunk && chunk.trim())
|
|
254
256
|
deps.chunkSender?.(sessionId, receiverId, chunk, messageId);
|
|
@@ -288,7 +290,9 @@ export function createZiggsEffectHandler(deps) {
|
|
|
288
290
|
// visible to this agent across turns, not to other chat parties.
|
|
289
291
|
try {
|
|
290
292
|
if (effect.entry.kind === 'thought' && typeof effect.entry.text === 'string') {
|
|
291
|
-
await deps.artifactsClient.recordThought(effect.sessionId, effect.entry.text
|
|
293
|
+
await deps.artifactsClient.recordThought(effect.sessionId, effect.entry.text, {
|
|
294
|
+
idempotencyKey: effect.idempotencyKey,
|
|
295
|
+
});
|
|
292
296
|
}
|
|
293
297
|
else {
|
|
294
298
|
await deps.artifactsClient.write({
|
|
@@ -296,6 +300,7 @@ export function createZiggsEffectHandler(deps) {
|
|
|
296
300
|
text: JSON.stringify(effect.entry),
|
|
297
301
|
content_type: String(effect.entry.kind ?? 'event'),
|
|
298
302
|
visibility: 'agent-private',
|
|
303
|
+
idempotencyKey: effect.idempotencyKey,
|
|
299
304
|
});
|
|
300
305
|
}
|
|
301
306
|
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { type Creds } from '@ziggs-ai/api-client';
|
|
2
|
+
import { SeenMessages } from './SeenMessages.js';
|
|
3
|
+
/**
|
|
4
|
+
* Per-agent inbox long-poll loop (delivery law, phase 4). This is the ONLY way
|
|
5
|
+
* a hosted agent consumes: `GET /inbox?wait=` → grant-fenced reads of the delta
|
|
6
|
+
* → deliver into the host's lane/tick machinery → `POST /inbox/ack`. No socket,
|
|
7
|
+
* no push, no separate catch-up channel — a crash-replay is just the next
|
|
8
|
+
* iteration (the cursor is server-side), and writes carry idempotency keys so
|
|
9
|
+
* re-processing a redelivered window is wasteful, never harmful. Act-before-ack
|
|
10
|
+
* is the one ordering rule.
|
|
11
|
+
*/
|
|
12
|
+
export interface InboxLoopDeps {
|
|
13
|
+
creds: Creds;
|
|
14
|
+
ownAgentId: string;
|
|
15
|
+
baseUrl?: string;
|
|
16
|
+
seen: SeenMessages;
|
|
17
|
+
onMessage: (text: string, metadata: Record<string, unknown>) => Promise<void>;
|
|
18
|
+
onResourceEvent: (event: Record<string, unknown>) => Promise<void>;
|
|
19
|
+
label?: string;
|
|
20
|
+
}
|
|
21
|
+
export interface InboxPassResult {
|
|
22
|
+
scopes: number;
|
|
23
|
+
messagesDelivered: number;
|
|
24
|
+
resourceEvents: number;
|
|
25
|
+
acked: number;
|
|
26
|
+
truncated: boolean;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* One long-poll pass: hold up to `waitSeconds` for new inbox content, then read
|
|
30
|
+
* the deltas, deliver them into the host, and ack. Returns what it delivered.
|
|
31
|
+
* Reused each iteration of {@link runInboxLoop}; also usable as a single catch-up
|
|
32
|
+
* read (`waitSeconds: 0`) at bootstrap.
|
|
33
|
+
*/
|
|
34
|
+
export declare function pollInboxOnce(deps: InboxLoopDeps, waitSeconds?: number): Promise<InboxPassResult>;
|
|
35
|
+
export interface InboxLoopHandle {
|
|
36
|
+
/** Stop the loop after the current pass; resolves when it has fully exited. */
|
|
37
|
+
stop(): Promise<void>;
|
|
38
|
+
/** True while the loop is running. */
|
|
39
|
+
readonly running: boolean;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Run {@link pollInboxOnce} forever (until stopped). Each iteration long-polls
|
|
43
|
+
* for `waitSeconds`; a passing envelope returns instantly, an empty one holds.
|
|
44
|
+
* Errors retry with exponential backoff + jitter and never exit the loop —
|
|
45
|
+
* there is nothing to resynchronize (the cursor is server-side), so recovery is
|
|
46
|
+
* just the next iteration.
|
|
47
|
+
*/
|
|
48
|
+
export declare function runInboxLoop(deps: InboxLoopDeps, opts?: {
|
|
49
|
+
waitSeconds?: number;
|
|
50
|
+
}): InboxLoopHandle;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { ContextReadClient, GrantsClient, InboxClient, getChatsForAgreement,
|
|
1
|
+
import { ContextReadClient, GrantsClient, InboxClient, getChatsForAgreement, } from '@ziggs-ai/api-client';
|
|
2
2
|
import { runtimeLog } from '../shared/runtimeLog.js';
|
|
3
3
|
const READ_PAGE = 100;
|
|
4
4
|
function log(label, msg) {
|
|
@@ -7,19 +7,6 @@ function log(label, msg) {
|
|
|
7
7
|
function warn(label, msg) {
|
|
8
8
|
runtimeLog.warn(label, msg);
|
|
9
9
|
}
|
|
10
|
-
const TERMINAL_AGREEMENT_STATUSES = new Set(['fulfilled', 'cancelled', 'refunded']);
|
|
11
|
-
/** ZIG-832 P4: is this agreement finished (so its backlog must not be re-run)? */
|
|
12
|
-
async function isTerminalAgreement(agreementId, creds) {
|
|
13
|
-
try {
|
|
14
|
-
const res = (await getAgreementStatus(agreementId, creds));
|
|
15
|
-
const status = res?.status ?? res?.agreement?.status;
|
|
16
|
-
return status != null && TERMINAL_AGREEMENT_STATUSES.has(status);
|
|
17
|
-
}
|
|
18
|
-
catch {
|
|
19
|
-
// On lookup failure, don't skip — fall through to normal delivery.
|
|
20
|
-
return false;
|
|
21
|
-
}
|
|
22
|
-
}
|
|
23
10
|
/** Map Mongo message row → wire metadata for normalizeIncomingEvent. */
|
|
24
11
|
function rowToMetadata(row, chatId) {
|
|
25
12
|
const senderRaw = row.sender;
|
|
@@ -47,7 +34,7 @@ function rowToMetadata(row, chatId) {
|
|
|
47
34
|
text: row.text,
|
|
48
35
|
};
|
|
49
36
|
}
|
|
50
|
-
async function chatIdsForScope(scope, creds,
|
|
37
|
+
async function chatIdsForScope(scope, creds, grants) {
|
|
51
38
|
const { kind, id } = scope.scope;
|
|
52
39
|
switch (kind) {
|
|
53
40
|
case 'chat':
|
|
@@ -57,8 +44,8 @@ async function chatIdsForScope(scope, creds, baseUrl, grants) {
|
|
|
57
44
|
return [...new Set(links.map((l) => l.chatId).filter(Boolean))];
|
|
58
45
|
}
|
|
59
46
|
case 'org': {
|
|
60
|
-
// All pages of live chat grants — an org-scope
|
|
61
|
-
//
|
|
47
|
+
// All pages of live chat grants — an org-scope read must not miss chats
|
|
48
|
+
// hidden behind pagination (GET /grants defaults to one page).
|
|
62
49
|
const items = await grants.listAllGrants({
|
|
63
50
|
scopeKind: 'chat',
|
|
64
51
|
health: 'active',
|
|
@@ -71,7 +58,7 @@ async function chatIdsForScope(scope, creds, baseUrl, grants) {
|
|
|
71
58
|
}
|
|
72
59
|
async function deliverMessagesForScope(deps, scope, chatIds, reader, countCap) {
|
|
73
60
|
let delivered = 0;
|
|
74
|
-
const label = deps.label ?? '
|
|
61
|
+
const label = deps.label ?? 'inboxLoop';
|
|
75
62
|
const expected = scope.newMessages;
|
|
76
63
|
let remaining = expected >= countCap ? Number.POSITIVE_INFINITY : expected;
|
|
77
64
|
for (const chatId of chatIds) {
|
|
@@ -113,49 +100,31 @@ async function deliverMessagesForScope(deps, scope, chatIds, reader, countCap) {
|
|
|
113
100
|
return delivered;
|
|
114
101
|
}
|
|
115
102
|
/**
|
|
116
|
-
*
|
|
117
|
-
*
|
|
118
|
-
*
|
|
103
|
+
* One long-poll pass: hold up to `waitSeconds` for new inbox content, then read
|
|
104
|
+
* the deltas, deliver them into the host, and ack. Returns what it delivered.
|
|
105
|
+
* Reused each iteration of {@link runInboxLoop}; also usable as a single catch-up
|
|
106
|
+
* read (`waitSeconds: 0`) at bootstrap.
|
|
119
107
|
*/
|
|
120
|
-
export async function
|
|
121
|
-
const label = deps.label ?? '
|
|
108
|
+
export async function pollInboxOnce(deps, waitSeconds = 0) {
|
|
109
|
+
const label = deps.label ?? 'inboxLoop';
|
|
122
110
|
const inbox = new InboxClient(deps.creds.operatorKey, deps.creds.agentId, deps.baseUrl);
|
|
123
111
|
const reader = new ContextReadClient(deps.creds.operatorKey, deps.creds.agentId, deps.baseUrl);
|
|
124
112
|
const grants = new GrantsClient(deps.creds.operatorKey, deps.creds.agentId, deps.baseUrl);
|
|
125
|
-
const envelope = await inbox.getInbox();
|
|
113
|
+
const envelope = await inbox.getInbox(waitSeconds > 0 ? { waitSeconds } : {});
|
|
126
114
|
const countCap = envelope.countCap;
|
|
127
115
|
let messagesDelivered = 0;
|
|
128
116
|
let resourceEvents = 0;
|
|
129
117
|
const toAck = [];
|
|
130
118
|
if (envelope.truncatedScopes > 0) {
|
|
131
|
-
// Informational only — does NOT block acking the scopes
|
|
132
|
-
//
|
|
133
|
-
//
|
|
134
|
-
//
|
|
135
|
-
//
|
|
136
|
-
// acking here meant a busy long-lived agent (reach > cap) would never ack
|
|
137
|
-
// anything and re-deliver its entire backlog on every reconnect forever.
|
|
119
|
+
// Informational only — does NOT block acking the scopes returned below.
|
|
120
|
+
// truncatedScopes just means the caller's total reach exceeded the server's
|
|
121
|
+
// per-call scope cap; each returned scope is still an independently accurate,
|
|
122
|
+
// safe-to-ack unit of news. Never acking here would make a busy agent
|
|
123
|
+
// (reach > cap) re-deliver its whole backlog every pass forever.
|
|
138
124
|
warn(label, `inbox truncatedScopes=${envelope.truncatedScopes} (reach exceeds per-call cap — examined scopes are still acked normally)`);
|
|
139
125
|
}
|
|
140
126
|
for (const scope of envelope.scopes) {
|
|
141
|
-
|
|
142
|
-
// re-deliver an already-finished agreement's backlog, re-running a completed
|
|
143
|
-
// pipeline and burning tokens. If this is an agreement scope that has reached
|
|
144
|
-
// a terminal status, ack it (so it drops out of future catch-ups) and skip
|
|
145
|
-
// re-delivery. Only agreement scopes — chat scopes may carry genuinely new
|
|
146
|
-
// conversation, so those are never skipped on status. (The deeper fix, acking
|
|
147
|
-
// the watermark on the live-push path so nothing re-delivers, is tracked
|
|
148
|
-
// separately.)
|
|
149
|
-
if (scope.scope.kind === 'agreement' &&
|
|
150
|
-
(scope.newMessages > 0 || scope.newArtifacts > 0) &&
|
|
151
|
-
(await isTerminalAgreement(scope.scope.id, deps.creds))) {
|
|
152
|
-
if (scope.latestAt) {
|
|
153
|
-
toAck.push({ kind: scope.scope.kind, id: scope.scope.id, upTo: scope.latestAt });
|
|
154
|
-
}
|
|
155
|
-
log(label, `scope agreement:${scope.scope.id} terminal — acked, not re-delivered`);
|
|
156
|
-
continue;
|
|
157
|
-
}
|
|
158
|
-
const chatIds = await chatIdsForScope(scope, deps.creds, deps.baseUrl, grants);
|
|
127
|
+
const chatIds = await chatIdsForScope(scope, deps.creds, grants);
|
|
159
128
|
if (!chatIds.length && (scope.newMessages > 0 || scope.newArtifacts > 0)) {
|
|
160
129
|
warn(label, `no chat ids for scope ${scope.scope.kind}:${scope.scope.id} (${scope.newMessages} msgs)`);
|
|
161
130
|
}
|
|
@@ -201,9 +170,6 @@ export async function runInboxCatchUp(deps) {
|
|
|
201
170
|
acked = toAck.length;
|
|
202
171
|
log(label, `acked ${acked} scope(s)`);
|
|
203
172
|
}
|
|
204
|
-
if (messagesDelivered > 0 || resourceEvents > 0) {
|
|
205
|
-
log(label, `catch-up complete: ${messagesDelivered} message(s), ${resourceEvents} resource hint(s)`);
|
|
206
|
-
}
|
|
207
173
|
return {
|
|
208
174
|
scopes: envelope.scopes.length,
|
|
209
175
|
messagesDelivered,
|
|
@@ -212,3 +178,48 @@ export async function runInboxCatchUp(deps) {
|
|
|
212
178
|
truncated: envelope.truncatedScopes > 0,
|
|
213
179
|
};
|
|
214
180
|
}
|
|
181
|
+
const DEFAULT_WAIT_SECONDS = 25;
|
|
182
|
+
const ERROR_BACKOFF_BASE_MS = 1_000;
|
|
183
|
+
const ERROR_BACKOFF_MAX_MS = 30_000;
|
|
184
|
+
function sleep(ms) {
|
|
185
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
186
|
+
}
|
|
187
|
+
/**
|
|
188
|
+
* Run {@link pollInboxOnce} forever (until stopped). Each iteration long-polls
|
|
189
|
+
* for `waitSeconds`; a passing envelope returns instantly, an empty one holds.
|
|
190
|
+
* Errors retry with exponential backoff + jitter and never exit the loop —
|
|
191
|
+
* there is nothing to resynchronize (the cursor is server-side), so recovery is
|
|
192
|
+
* just the next iteration.
|
|
193
|
+
*/
|
|
194
|
+
export function runInboxLoop(deps, opts = {}) {
|
|
195
|
+
const label = deps.label ?? 'inboxLoop';
|
|
196
|
+
const waitSeconds = opts.waitSeconds ?? DEFAULT_WAIT_SECONDS;
|
|
197
|
+
let running = true;
|
|
198
|
+
let consecutiveErrors = 0;
|
|
199
|
+
const done = (async () => {
|
|
200
|
+
while (running) {
|
|
201
|
+
try {
|
|
202
|
+
await pollInboxOnce(deps, waitSeconds);
|
|
203
|
+
consecutiveErrors = 0;
|
|
204
|
+
}
|
|
205
|
+
catch (err) {
|
|
206
|
+
if (!running)
|
|
207
|
+
break;
|
|
208
|
+
consecutiveErrors++;
|
|
209
|
+
const backoff = Math.min(ERROR_BACKOFF_MAX_MS, ERROR_BACKOFF_BASE_MS * 2 ** (consecutiveErrors - 1));
|
|
210
|
+
const jitter = backoff * (0.5 + (consecutiveErrors % 7) / 14); // deterministic-ish, no Math.random
|
|
211
|
+
warn(label, `inbox loop pass failed (#${consecutiveErrors}): ${err?.message ?? err} — retrying in ~${Math.round(jitter)}ms`);
|
|
212
|
+
await sleep(jitter);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
})();
|
|
216
|
+
return {
|
|
217
|
+
get running() {
|
|
218
|
+
return running;
|
|
219
|
+
},
|
|
220
|
+
async stop() {
|
|
221
|
+
running = false;
|
|
222
|
+
await done.catch(() => { });
|
|
223
|
+
},
|
|
224
|
+
};
|
|
225
|
+
}
|
|
@@ -53,7 +53,7 @@ async function runFleet(configs, { operatorKey, wsUrl = process.env.WS_URL, port
|
|
|
53
53
|
pool.startControl({ wsUrl, operatorKey: controlKey });
|
|
54
54
|
}
|
|
55
55
|
else {
|
|
56
|
-
runtimeLog.warn(label, 'No operatorKey provided — skipping
|
|
56
|
+
runtimeLog.warn(label, 'No operatorKey provided — skipping fleet inbox poll (no agents will be lazily started)');
|
|
57
57
|
}
|
|
58
58
|
let primary = null;
|
|
59
59
|
if (resolvedPrimaryConfig) {
|
|
@@ -15,8 +15,9 @@ export declare class TaskService {
|
|
|
15
15
|
requireMidWorkPlanAck?: boolean;
|
|
16
16
|
assigneeId?: string;
|
|
17
17
|
inputArtifactIds?: string[];
|
|
18
|
+
idempotencyKey?: string;
|
|
18
19
|
}): Promise<Task>;
|
|
19
|
-
updateState(taskId: string, state: TaskState, result?: unknown): Promise<Task>;
|
|
20
|
+
updateState(taskId: string, state: TaskState, result?: unknown, idempotencyKey?: string): Promise<Task>;
|
|
20
21
|
updatePlanStep(taskId: string, stepId: string, status: string, result?: unknown): Promise<Task>;
|
|
21
22
|
cancel(taskId: string): Promise<Task>;
|
|
22
23
|
report(taskId: string, message?: string): Promise<Task>;
|
|
@@ -57,9 +57,12 @@ export class TaskService {
|
|
|
57
57
|
throw error;
|
|
58
58
|
}
|
|
59
59
|
}
|
|
60
|
-
async updateState(taskId, state, result) {
|
|
60
|
+
async updateState(taskId, state, result, idempotencyKey) {
|
|
61
61
|
try {
|
|
62
|
-
const data =
|
|
62
|
+
const data = {
|
|
63
|
+
...(result !== undefined ? { result: safeJsonStringify(result) } : {}),
|
|
64
|
+
...(idempotencyKey ? { idempotencyKey } : {}),
|
|
65
|
+
};
|
|
63
66
|
const updated = await updateTaskState(taskId, state, data, this.creds);
|
|
64
67
|
runtimeLog.info('TaskService', `Task state: ${taskId} → ${state}`);
|
|
65
68
|
return updated;
|
|
@@ -19,6 +19,10 @@ export { PROTOCOL_TOOLS, agreementProposeTool, agreementSubcontractTool, agreeme
|
|
|
19
19
|
export { dispatchProtocolOp } from './tier1/protocolRunner.js';
|
|
20
20
|
export { DISCOVERY_TOOLS, agentSearchTool, agentGetTool } from './tier2/discoveryTools.js';
|
|
21
21
|
export { MARKETPLACE_TOOLS, marketplaceViewTool } from './tier2/marketplaceTools.js';
|
|
22
|
-
export { PAYMENT_TOOLS, paymentBalanceTool, paymentTransferTool, paymentWaitForApprovalTool, paymentHoldTool, paymentReleaseTool, paymentResolveWalletTool, paymentIssueGrantTool, paymentAttenuateGrantTool, paymentRevokeGrantTool,
|
|
23
|
-
export { CONNECTION_TOOLS, connectionProxyTool, connectionListGrantsTool, } from './tier2/connectionTools.js';
|
|
24
|
-
export { CONTEXT_TOOLS,
|
|
22
|
+
export { PAYMENT_TOOLS, paymentBalanceTool, paymentTransferTool, paymentWaitForApprovalTool, paymentHoldTool, paymentReleaseTool, paymentResolveWalletTool, paymentIssueGrantTool, paymentAttenuateGrantTool, paymentRevokeGrantTool, } from './tier2/paymentTools.js';
|
|
23
|
+
export { CONNECTION_TOOLS, connectionProxyTool, connectionListGrantsTool, requestConnectionTool, } from './tier2/connectionTools.js';
|
|
24
|
+
export { CONTEXT_TOOLS, contextReadTool, contextDelegateTool, contextExpandReachTool, contextDiscoverGrantableTool, } from './tier2/contextTools.js';
|
|
25
|
+
export { GRANTS_TOOLS, listGrantsTool } from './tier2/grantsTools.js';
|
|
26
|
+
export { LINK_TOOLS, requestLinkTool, createLinkInviteTool, claimLinkInviteTool, listLinksTool, revokeLinkTool, } from './tier2/linkTools.js';
|
|
27
|
+
export { ARTIFACT_TOOLS, recordArtifactTool } from './tier2/artifactTools.js';
|
|
28
|
+
export { CHAT_TOOLS, openConversationTool } from './tier2/chatTools.js';
|
|
@@ -21,6 +21,16 @@ export { dispatchProtocolOp } from './tier1/protocolRunner.js';
|
|
|
21
21
|
// ── Tier 2: capability bundles ──────────────────────────────────────────────
|
|
22
22
|
export { DISCOVERY_TOOLS, agentSearchTool, agentGetTool } from './tier2/discoveryTools.js';
|
|
23
23
|
export { MARKETPLACE_TOOLS, marketplaceViewTool } from './tier2/marketplaceTools.js';
|
|
24
|
-
export { PAYMENT_TOOLS, paymentBalanceTool, paymentTransferTool, paymentWaitForApprovalTool, paymentHoldTool, paymentReleaseTool, paymentResolveWalletTool, paymentIssueGrantTool, paymentAttenuateGrantTool, paymentRevokeGrantTool,
|
|
25
|
-
export { CONNECTION_TOOLS, connectionProxyTool, connectionListGrantsTool, } from './tier2/connectionTools.js';
|
|
26
|
-
export { CONTEXT_TOOLS,
|
|
24
|
+
export { PAYMENT_TOOLS, paymentBalanceTool, paymentTransferTool, paymentWaitForApprovalTool, paymentHoldTool, paymentReleaseTool, paymentResolveWalletTool, paymentIssueGrantTool, paymentAttenuateGrantTool, paymentRevokeGrantTool, } from './tier2/paymentTools.js';
|
|
25
|
+
export { CONNECTION_TOOLS, connectionProxyTool, connectionListGrantsTool, requestConnectionTool, } from './tier2/connectionTools.js';
|
|
26
|
+
export { CONTEXT_TOOLS, contextReadTool, contextDelegateTool, contextExpandReachTool, contextDiscoverGrantableTool, } from './tier2/contextTools.js';
|
|
27
|
+
// ZIG-893: one grant lister across every rail (retires context_discover +
|
|
28
|
+
// payment_list_grants). Always-available on the hosted agent — see CLAUDE_SDK_TOOLS.
|
|
29
|
+
export { GRANTS_TOOLS, listGrantsTool } from './tier2/grantsTools.js';
|
|
30
|
+
// ZIG-898: links (agreements with engagementKind 'link') — cross-org reach.
|
|
31
|
+
export { LINK_TOOLS, requestLinkTool, createLinkInviteTool, claimLinkInviteTool, listLinksTool, revokeLinkTool, } from './tier2/linkTools.js';
|
|
32
|
+
// ZIG-899: deliberate artifact recording (the model decides "record this").
|
|
33
|
+
export { ARTIFACT_TOOLS, recordArtifactTool } from './tier2/artifactTools.js';
|
|
34
|
+
// ZIG-900 decision: open_conversation only — chat listing stays a context-rail
|
|
35
|
+
// read (list_grants scopeKind=chat), no duplicate lister (ZIG-626 rule).
|
|
36
|
+
export { CHAT_TOOLS, openConversationTool } from './tier2/chatTools.js';
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { AgreementService } from '../../agreements/AgreementService.js';
|
|
2
2
|
import type { TaskService } from '../../tasks/TaskService.js';
|
|
3
|
+
import type { WakeWriteKeys } from '../../../types.js';
|
|
3
4
|
import type { ContextReadClient } from '@ziggs-ai/api-client';
|
|
4
5
|
interface Services {
|
|
5
6
|
agreementService: AgreementService;
|
|
@@ -35,5 +36,5 @@ interface ProtocolPayload {
|
|
|
35
36
|
})[];
|
|
36
37
|
[key: string]: unknown;
|
|
37
38
|
}
|
|
38
|
-
export declare function dispatchProtocolOp(services: Services, payload: ProtocolPayload, chatId: string | null, agents?: unknown[], actingAgentId?: string | null): Promise<unknown>;
|
|
39
|
+
export declare function dispatchProtocolOp(services: Services, payload: ProtocolPayload, chatId: string | null, agents?: unknown[], actingAgentId?: string | null, wakeKeys?: WakeWriteKeys | null): Promise<unknown>;
|
|
39
40
|
export {};
|