@ziggs-ai/agent-sdk 0.5.0 → 0.7.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/dist/AgentHost.d.ts +22 -1
- package/dist/AgentHost.js +61 -13
- package/dist/agent/Agent.d.ts +9 -0
- package/dist/agent/ClaudeSdkAgent.d.ts +1 -1
- package/dist/agent/ClaudeSdkAgent.js +48 -13
- package/dist/agent/claudeCognition.js +1 -1
- package/dist/agent/claudeSdkProtocol.d.ts +1 -1
- package/dist/agent/claudeSdkProtocol.js +3 -3
- package/dist/agent/claudeSdkTools.d.ts +2 -2
- package/dist/agent/claudeSdkTools.js +12 -12
- package/dist/context/routingLabels.js +1 -1
- package/dist/defineAgent.d.ts +10 -5
- package/dist/defineAgent.js +16 -9
- package/dist/index.d.ts +1 -1
- package/dist/index.js +2 -2
- package/dist/runtime/deriveRespondsToProposals.d.ts +1 -1
- package/dist/runtime/deriveRespondsToProposals.js +1 -1
- package/dist/runtime/kickoff.js +3 -3
- package/dist/runtime/runTurn.js +54 -9
- package/dist/runtime/taskIntake.d.ts +18 -0
- package/dist/runtime/taskIntake.js +113 -0
- package/dist/server/ConnectionPool.d.ts +9 -2
- package/dist/server/ConnectionPool.js +18 -2
- package/dist/server/ZiggsEffectHandler.d.ts +1 -1
- package/dist/server/ZiggsEffectHandler.js +2 -2
- package/dist/server/agreements/AgreementService.d.ts +13 -0
- package/dist/server/agreements/AgreementService.js +16 -1
- package/dist/server/inboxLoop.d.ts +31 -11
- package/dist/server/inboxLoop.js +147 -104
- package/dist/server/runLauncher.js +9 -3
- package/dist/server/staleBacklogGate.d.ts +9 -5
- package/dist/server/staleBacklogGate.js +17 -37
- package/dist/server/tools/index.d.ts +2 -2
- package/dist/server/tools/index.js +6 -4
- package/dist/server/tools/tier1/protocolRunner.d.ts +1 -1
- package/dist/server/tools/tier1/protocolRunner.js +30 -2
- package/dist/server/tools/tier1/protocolTools.d.ts +5 -4
- package/dist/server/tools/tier1/protocolTools.js +10 -9
- package/dist/server/tools/tier2/chatTools.js +1 -1
- package/dist/server/tools/tier2/connectionTools.js +2 -2
- package/dist/server/tools/tier2/linkTools.d.ts +2 -5
- package/dist/server/tools/tier2/linkTools.js +7 -6
- package/dist/server/tools/tier2/marketplaceTools.d.ts +1 -1
- package/dist/server/tools/tier2/marketplaceTools.js +9 -22
- package/dist/tasks/protocolRegistry.d.ts +5 -4
- package/dist/tasks/protocolRegistry.js +5 -4
- package/dist/tools/recordReport.d.ts +1 -1
- package/dist/types.d.ts +4 -3
- package/package.json +2 -2
package/dist/server/inboxLoop.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import { ContextReadClient,
|
|
1
|
+
import { ContextReadClient, InboxClient, } from '@ziggs-ai/api-client';
|
|
2
2
|
import { runtimeLog } from '../shared/runtimeLog.js';
|
|
3
|
+
import { isRateLimited } from '@ziggs-ai/api-client';
|
|
3
4
|
const READ_PAGE = 100;
|
|
4
5
|
function log(label, msg) {
|
|
5
6
|
runtimeLog.info(label, msg);
|
|
@@ -7,67 +8,61 @@ function log(label, msg) {
|
|
|
7
8
|
function warn(label, msg) {
|
|
8
9
|
runtimeLog.warn(label, msg);
|
|
9
10
|
}
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
}
|
|
19
|
-
case 'org': {
|
|
20
|
-
// All pages of live chat grants — an org-scope read must not miss chats
|
|
21
|
-
// hidden behind pagination (GET /grants defaults to one page).
|
|
22
|
-
const items = await grants.listAllGrants({
|
|
23
|
-
scopeKind: 'chat',
|
|
24
|
-
health: 'active',
|
|
25
|
-
});
|
|
26
|
-
return [...new Set(items.map((g) => g.scope.id))];
|
|
27
|
-
}
|
|
28
|
-
default:
|
|
29
|
-
return [];
|
|
30
|
-
}
|
|
31
|
-
}
|
|
32
|
-
async function deliverMessagesForScope(deps, scope, chatIds, reader, countCap) {
|
|
11
|
+
/**
|
|
12
|
+
* Read the messages behind one chat's deliveries and hand them to the host.
|
|
13
|
+
*
|
|
14
|
+
* The envelope says which chat and how many, not the bodies — so this still
|
|
15
|
+
* reads through `/context/read`, where the agent's own grants fence what it is
|
|
16
|
+
* allowed to see. A delivery reference is an address, never an authorisation.
|
|
17
|
+
*/
|
|
18
|
+
async function deliverMessagesForChat(deps, news, earliestTs, reader) {
|
|
33
19
|
let delivered = 0;
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
const
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
via: `chat:${chatId}`,
|
|
20
|
+
let remaining = news.count;
|
|
21
|
+
// `after` is exclusive, so step back a millisecond to include the oldest
|
|
22
|
+
// delivery itself. Re-reading a neighbour that shares its timestamp is
|
|
23
|
+
// harmless — `seen` dedups by messageId.
|
|
24
|
+
let after = new Date(Date.parse(earliestTs) - 1).toISOString();
|
|
25
|
+
let pages = 0;
|
|
26
|
+
const maxPages = 20;
|
|
27
|
+
while (remaining > 0 && pages < maxPages) {
|
|
28
|
+
const limit = Math.min(READ_PAGE, remaining);
|
|
29
|
+
let page;
|
|
30
|
+
try {
|
|
31
|
+
page = await reader.read('messages', {
|
|
32
|
+
via: `chat:${news.chatId}`,
|
|
47
33
|
after,
|
|
48
34
|
direction: 'forward',
|
|
49
35
|
limit,
|
|
50
36
|
});
|
|
51
|
-
for (const row of page.items) {
|
|
52
|
-
const messageId = String(row.messageId ?? '');
|
|
53
|
-
if (deps.seen.has(messageId))
|
|
54
|
-
continue;
|
|
55
|
-
const text = typeof row.text === 'string' ? row.text : '';
|
|
56
|
-
if (!text.trim())
|
|
57
|
-
continue;
|
|
58
|
-
await deps.onMessage(text, { ...row, chatId });
|
|
59
|
-
delivered++;
|
|
60
|
-
if (remaining !== Number.POSITIVE_INFINITY)
|
|
61
|
-
remaining--;
|
|
62
|
-
}
|
|
63
|
-
if (!page.hasMore || !page.latestSequence)
|
|
64
|
-
break;
|
|
65
|
-
after = page.latestSequence;
|
|
66
|
-
pages++;
|
|
67
37
|
}
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
38
|
+
catch (err) {
|
|
39
|
+
const status = err.status;
|
|
40
|
+
if (status !== 403 && status !== 404)
|
|
41
|
+
throw err;
|
|
42
|
+
// Addressed but not readable. Since the delivery log and the grants are
|
|
43
|
+
// now separate, the two can disagree — a grant revoked between the write
|
|
44
|
+
// and this read, or a chat since deleted. Skipping keeps the pass alive
|
|
45
|
+
// so the rest of the mail is delivered and the watermark still advances;
|
|
46
|
+
// failing here would wedge the agent on one unreadable item forever,
|
|
47
|
+
// re-woken by the same delivery on every pass.
|
|
48
|
+
warn(deps.label ?? 'inboxLoop', `chat ${news.chatId}: addressed but not readable (${status}) — skipping ${remaining} delivery/deliveries`);
|
|
49
|
+
return delivered;
|
|
50
|
+
}
|
|
51
|
+
for (const row of page.items) {
|
|
52
|
+
const messageId = String(row.messageId ?? '');
|
|
53
|
+
if (deps.seen.has(messageId))
|
|
54
|
+
continue;
|
|
55
|
+
const text = typeof row.text === 'string' ? row.text : '';
|
|
56
|
+
if (!text.trim())
|
|
57
|
+
continue;
|
|
58
|
+
await deps.onMessage(text, { ...row, chatId: news.chatId });
|
|
59
|
+
delivered++;
|
|
60
|
+
remaining--;
|
|
61
|
+
}
|
|
62
|
+
if (!page.hasMore || !page.latestSequence)
|
|
63
|
+
break;
|
|
64
|
+
after = page.latestSequence;
|
|
65
|
+
pages++;
|
|
71
66
|
}
|
|
72
67
|
return delivered;
|
|
73
68
|
}
|
|
@@ -81,51 +76,77 @@ export async function pollInboxOnce(deps, waitSeconds = 0) {
|
|
|
81
76
|
const label = deps.label ?? 'inboxLoop';
|
|
82
77
|
const inbox = new InboxClient(deps.creds.operatorKey, deps.creds.agentId, deps.baseUrl);
|
|
83
78
|
const reader = new ContextReadClient(deps.creds.operatorKey, deps.creds.agentId, deps.baseUrl);
|
|
84
|
-
const grants = new GrantsClient(deps.creds.operatorKey, deps.creds.agentId, deps.baseUrl);
|
|
85
79
|
const envelope = await inbox.getInbox(waitSeconds > 0 ? { waitSeconds } : {});
|
|
86
|
-
const countCap = envelope.countCap;
|
|
87
80
|
let messagesDelivered = 0;
|
|
88
81
|
let resourceEvents = 0;
|
|
89
|
-
|
|
90
|
-
if (envelope.
|
|
91
|
-
// Informational only — does NOT block acking
|
|
92
|
-
//
|
|
93
|
-
//
|
|
94
|
-
|
|
95
|
-
// (reach > cap) re-deliver its whole backlog every pass forever.
|
|
96
|
-
warn(label, `inbox truncatedScopes=${envelope.truncatedScopes} (reach exceeds per-call cap — examined scopes are still acked normally)`);
|
|
82
|
+
let tasksDelivered = 0;
|
|
83
|
+
if (envelope.deliveriesCapped) {
|
|
84
|
+
// Informational only — does NOT block acking what WAS handed over. The
|
|
85
|
+
// tail stays unacked and arrives on the next pass; never acking here would
|
|
86
|
+
// make a badly backed-up agent replay its whole backlog forever.
|
|
87
|
+
warn(label, 'inbox deliveriesCapped (more mail than one envelope carries — the tail follows on the next pass)');
|
|
97
88
|
}
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
89
|
+
// Oldest delivery per chat: where to start reading that chat's messages.
|
|
90
|
+
const earliestByChat = new Map();
|
|
91
|
+
for (const d of envelope.deliveries) {
|
|
92
|
+
if (!d.chatId)
|
|
93
|
+
continue;
|
|
94
|
+
const seen = earliestByChat.get(d.chatId);
|
|
95
|
+
if (!seen || d.ts < seen)
|
|
96
|
+
earliestByChat.set(d.chatId, d.ts);
|
|
97
|
+
}
|
|
98
|
+
for (const news of envelope.chats) {
|
|
99
|
+
const earliest = earliestByChat.get(news.chatId) ?? news.latestAt;
|
|
100
|
+
const n = await deliverMessagesForChat(deps, news, earliest, reader);
|
|
101
|
+
messagesDelivered += n;
|
|
102
|
+
if (n > 0)
|
|
103
|
+
log(label, `chat ${news.chatId} delivered ${n} message(s)`);
|
|
104
|
+
}
|
|
105
|
+
// Everything that is not chat text is handed over as a resource event, from
|
|
106
|
+
// the delivery itself — the reference already carries the ids the host needs,
|
|
107
|
+
// so nothing has to be re-derived to name what changed.
|
|
108
|
+
for (const d of envelope.deliveries) {
|
|
109
|
+
if (d.kind === 'message')
|
|
110
|
+
continue;
|
|
111
|
+
const key = `delivery:${d.kind}:${d.resourceId}:${d.ts}`;
|
|
112
|
+
if (deps.seen.has(key))
|
|
113
|
+
continue;
|
|
114
|
+
deps.seen.mark(key);
|
|
115
|
+
await deps.onResourceEvent({
|
|
116
|
+
kind: d.kind,
|
|
117
|
+
ts: d.ts,
|
|
118
|
+
resourceId: d.resourceId,
|
|
119
|
+
...(d.chatId ? { chatId: d.chatId } : {}),
|
|
120
|
+
...(d.agreementId ? { agreementId: d.agreementId } : {}),
|
|
121
|
+
...(d.taskId ? { taskId: d.taskId } : {}),
|
|
122
|
+
change: 'created',
|
|
123
|
+
});
|
|
124
|
+
resourceEvents++;
|
|
125
|
+
}
|
|
126
|
+
// ZIG-973: the work channel. An open assigned task is redelivered on every
|
|
127
|
+
// pass until it closes (that IS the durable-wait contract), so dedup on
|
|
128
|
+
// taskId+updatedAt — a genuinely touched task wakes again, an untouched one
|
|
129
|
+
// doesn't storm the lane every long-poll.
|
|
130
|
+
for (const task of envelope.tasksAwaitingMe ?? []) {
|
|
131
|
+
const key = `task:${task.taskId}:${task.updatedAt ?? ''}`;
|
|
132
|
+
if (deps.seen.has(key))
|
|
133
|
+
continue;
|
|
134
|
+
deps.seen.mark(key);
|
|
135
|
+
if (deps.onTask) {
|
|
136
|
+
await deps.onTask(task);
|
|
137
|
+
tasksDelivered++;
|
|
126
138
|
}
|
|
127
139
|
}
|
|
140
|
+
if (envelope.truncatedTasks) {
|
|
141
|
+
warn(label, `inbox truncatedTasks=${envelope.truncatedTasks} (more assigned tasks than one pass surfaces — they appear as earlier ones close)`);
|
|
142
|
+
}
|
|
143
|
+
// Pending decisions are standing state, not deliveries: they keep appearing
|
|
144
|
+
// until answered, so dedup them the same way tasks are deduped.
|
|
128
145
|
for (const proposal of envelope.proposalsAwaitingMe) {
|
|
146
|
+
const key = `proposal:${proposal.agreementId}`;
|
|
147
|
+
if (deps.seen.has(key))
|
|
148
|
+
continue;
|
|
149
|
+
deps.seen.mark(key);
|
|
129
150
|
await deps.onResourceEvent({
|
|
130
151
|
kind: 'agreement',
|
|
131
152
|
ts: proposal.proposedAt ?? new Date().toISOString(),
|
|
@@ -136,18 +157,21 @@ export async function pollInboxOnce(deps, waitSeconds = 0) {
|
|
|
136
157
|
});
|
|
137
158
|
resourceEvents++;
|
|
138
159
|
}
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
160
|
+
// Act, then ack: everything above has been handed to the host, so the
|
|
161
|
+
// watermark may now move past it. A crash before this point redelivers.
|
|
162
|
+
let acked = false;
|
|
163
|
+
if (envelope.ackTo) {
|
|
164
|
+
await inbox.ack(envelope.ackTo);
|
|
165
|
+
acked = true;
|
|
166
|
+
log(label, `acked up to ${envelope.ackTo}`);
|
|
144
167
|
}
|
|
145
168
|
return {
|
|
146
|
-
|
|
169
|
+
deliveries: envelope.deliveries.length,
|
|
147
170
|
messagesDelivered,
|
|
148
171
|
resourceEvents,
|
|
172
|
+
tasksDelivered,
|
|
149
173
|
acked,
|
|
150
|
-
truncated: envelope.
|
|
174
|
+
truncated: envelope.deliveriesCapped,
|
|
151
175
|
};
|
|
152
176
|
}
|
|
153
177
|
const DEFAULT_WAIT_SECONDS = 25;
|
|
@@ -160,24 +184,43 @@ function sleep(ms) {
|
|
|
160
184
|
* Run {@link pollInboxOnce} forever (until stopped). Each iteration long-polls
|
|
161
185
|
* for `waitSeconds`; a passing envelope returns instantly, an empty one holds.
|
|
162
186
|
* Errors retry with exponential backoff + jitter and never exit the loop —
|
|
163
|
-
* there is nothing to resynchronize (the
|
|
164
|
-
* just the next iteration.
|
|
187
|
+
* there is nothing to resynchronize (the watermark is server-side), so recovery
|
|
188
|
+
* is just the next iteration.
|
|
189
|
+
*
|
|
190
|
+
* The FIRST pass is an immediate read. A held poll only answers to new mail, so
|
|
191
|
+
* a host that just started (or restarted after a crash) would otherwise sit
|
|
192
|
+
* waiting while the work it was already assigned — open tasks, pending
|
|
193
|
+
* decisions — went unseen until something unrelated arrived.
|
|
165
194
|
*/
|
|
166
195
|
export function runInboxLoop(deps, opts = {}) {
|
|
167
196
|
const label = deps.label ?? 'inboxLoop';
|
|
168
197
|
const waitSeconds = opts.waitSeconds ?? DEFAULT_WAIT_SECONDS;
|
|
169
198
|
let running = true;
|
|
170
199
|
let consecutiveErrors = 0;
|
|
200
|
+
let first = true;
|
|
171
201
|
const done = (async () => {
|
|
172
202
|
while (running) {
|
|
173
203
|
try {
|
|
174
|
-
await pollInboxOnce(deps, waitSeconds);
|
|
204
|
+
await pollInboxOnce(deps, first ? 0 : waitSeconds);
|
|
205
|
+
first = false;
|
|
175
206
|
consecutiveErrors = 0;
|
|
176
207
|
}
|
|
177
208
|
catch (err) {
|
|
178
209
|
if (!running)
|
|
179
210
|
break;
|
|
180
211
|
consecutiveErrors++;
|
|
212
|
+
// ZIG-1019: a 429 already told us how long to wait, and every refused
|
|
213
|
+
// call still counts against the cap — so the exponential ladder digs
|
|
214
|
+
// the hole deeper (1s, 2s, 4s… of pure refusals). Wait the server's
|
|
215
|
+
// number instead, and do not let a throttle inflate the ladder for the
|
|
216
|
+
// real errors that come after it.
|
|
217
|
+
const throttleMs = isRateLimited(err) ? (err.retryAfterMs ?? 30_000) : null;
|
|
218
|
+
if (throttleMs !== null) {
|
|
219
|
+
consecutiveErrors = 0;
|
|
220
|
+
warn(label, `inbox loop throttled: ${err?.message ?? err} — waiting ${Math.round(throttleMs)}ms as instructed`);
|
|
221
|
+
await sleep(throttleMs);
|
|
222
|
+
continue;
|
|
223
|
+
}
|
|
181
224
|
const backoff = Math.min(ERROR_BACKOFF_MAX_MS, ERROR_BACKOFF_BASE_MS * 2 ** (consecutiveErrors - 1));
|
|
182
225
|
const jitter = backoff * (0.5 + (consecutiveErrors % 7) / 14); // deterministic-ish, no Math.random
|
|
183
226
|
warn(label, `inbox loop pass failed (#${consecutiveErrors}): ${err?.message ?? err} — retrying in ~${Math.round(jitter)}ms`);
|
|
@@ -82,11 +82,17 @@ async function runFleet(configs, { operatorKey, wsUrl = process.env.WS_URL, port
|
|
|
82
82
|
}
|
|
83
83
|
}
|
|
84
84
|
if (preStart.length > 0) {
|
|
85
|
-
|
|
86
|
-
|
|
85
|
+
// Pinned: these agents are up because the operator named them
|
|
86
|
+
// (WAKE_AGENTS), not because work arrived, so the idle sweep must not
|
|
87
|
+
// retire them. It used to: an empty long-poll isn't "activity", so a quiet
|
|
88
|
+
// pre-started agent was torn down after idleTimeoutMs and only lazy start
|
|
89
|
+
// could bring it back — which at fleet size it cannot (ZIG-965 tail).
|
|
90
|
+
await Promise.all(preStart.map(id => pool.startAgent(id, { pinned: true })
|
|
91
|
+
.then(() => runtimeLog.debug(label, `Pre-started "${id}" (pinned)`))
|
|
87
92
|
.catch(err => runtimeLog.warn(label, `Could not pre-start "${id}": ${err.message}`))));
|
|
88
93
|
}
|
|
89
|
-
runtimeLog.info(label, `Ready — ${pool.size} agent(s) registered, ${pool.listActive().length} connected`
|
|
94
|
+
runtimeLog.info(label, `Ready — ${pool.size} agent(s) registered, ${pool.listActive().length} connected` +
|
|
95
|
+
(pool.listPinned().length ? ` (${pool.listPinned().length} pinned)` : ''));
|
|
90
96
|
process.on('SIGTERM', async () => {
|
|
91
97
|
runtimeLog.info(label, 'SIGTERM — shutting down');
|
|
92
98
|
pool.stopControl();
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
* `pool.startAgent()` / preStart are untouched, and an agent with ANY fresh item
|
|
19
19
|
* starts normally (its own inbox loop then drains the stale backlog too).
|
|
20
20
|
*/
|
|
21
|
-
import type {
|
|
21
|
+
import type { InboxOperatorAgentEntry } from '@ziggs-ai/api-client';
|
|
22
22
|
export declare const DEFAULT_REPLAY_MAX_AGE_MS: number;
|
|
23
23
|
export interface ReplayPolicy {
|
|
24
24
|
/** Skip slices whose newest actionable item is older than this. */
|
|
@@ -33,11 +33,15 @@ export interface ReplayPolicy {
|
|
|
33
33
|
export declare function parseReplayMaxAge(raw: string): number | null;
|
|
34
34
|
export declare function resolveReplayPolicy(env?: NodeJS.ProcessEnv): ReplayPolicy;
|
|
35
35
|
/**
|
|
36
|
-
* Newest actionable
|
|
37
|
-
*
|
|
38
|
-
*
|
|
36
|
+
* Newest actionable instant for an agent, from its operator-sweep entry.
|
|
37
|
+
*
|
|
38
|
+
* The sweep hands this over directly now — it is the agent's delivery
|
|
39
|
+
* watermark — where this used to fold together the max timestamp across every
|
|
40
|
+
* channel of a whole envelope. An agent surfaced only for open work carries no
|
|
41
|
+
* delivery instant: age is unprovable, so it is treated as fresh and started.
|
|
42
|
+
* Stale-skipping a half-finished task would strand it.
|
|
39
43
|
*/
|
|
40
|
-
export declare function newestActionableAt(
|
|
44
|
+
export declare function newestActionableAt(entry: InboxOperatorAgentEntry): number | null;
|
|
41
45
|
export declare function formatAge(ms: number): string;
|
|
42
46
|
export interface StaleBacklogGateOptions extends ReplayPolicy {
|
|
43
47
|
label?: string;
|
|
@@ -30,39 +30,19 @@ export function resolveReplayPolicy(env = process.env) {
|
|
|
30
30
|
return { maxAgeMs, replayAll };
|
|
31
31
|
}
|
|
32
32
|
/**
|
|
33
|
-
* Newest actionable
|
|
34
|
-
*
|
|
35
|
-
*
|
|
33
|
+
* Newest actionable instant for an agent, from its operator-sweep entry.
|
|
34
|
+
*
|
|
35
|
+
* The sweep hands this over directly now — it is the agent's delivery
|
|
36
|
+
* watermark — where this used to fold together the max timestamp across every
|
|
37
|
+
* channel of a whole envelope. An agent surfaced only for open work carries no
|
|
38
|
+
* delivery instant: age is unprovable, so it is treated as fresh and started.
|
|
39
|
+
* Stale-skipping a half-finished task would strand it.
|
|
36
40
|
*/
|
|
37
|
-
export function newestActionableAt(
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
const t = Date.parse(iso);
|
|
43
|
-
if (!Number.isNaN(t) && (newest == null || t > newest))
|
|
44
|
-
newest = t;
|
|
45
|
-
};
|
|
46
|
-
for (const scope of inbox.scopes ?? [])
|
|
47
|
-
consider(scope.latestAt);
|
|
48
|
-
for (const p of inbox.proposalsAwaitingMe ?? [])
|
|
49
|
-
consider(p.proposedAt);
|
|
50
|
-
for (const r of inbox.connectionRequestsAwaitingMe ?? [])
|
|
51
|
-
consider(r.requestedAt);
|
|
52
|
-
for (const r of inbox.mcpServerRequestsAwaitingMe ?? [])
|
|
53
|
-
consider(r.requestedAt);
|
|
54
|
-
return newest;
|
|
55
|
-
}
|
|
56
|
-
/** Unread items in a slice, for the summary line. Counts are backend-capped. */
|
|
57
|
-
function countItems(inbox) {
|
|
58
|
-
let items = 0;
|
|
59
|
-
for (const scope of inbox.scopes ?? []) {
|
|
60
|
-
items += (scope.newMessages ?? 0) + (scope.newArtifacts ?? 0);
|
|
61
|
-
}
|
|
62
|
-
items += (inbox.proposalsAwaitingMe ?? []).length;
|
|
63
|
-
items += (inbox.connectionRequestsAwaitingMe ?? []).length;
|
|
64
|
-
items += (inbox.mcpServerRequestsAwaitingMe ?? []).length;
|
|
65
|
-
return items;
|
|
41
|
+
export function newestActionableAt(entry) {
|
|
42
|
+
if (!entry.deliveredUpTo)
|
|
43
|
+
return null;
|
|
44
|
+
const t = Date.parse(entry.deliveredUpTo);
|
|
45
|
+
return Number.isNaN(t) ? null : t;
|
|
66
46
|
}
|
|
67
47
|
export function formatAge(ms) {
|
|
68
48
|
if (ms >= 86_400_000)
|
|
@@ -86,7 +66,6 @@ export function createStaleBacklogGate({ maxAgeMs, replayAll, label = 'launcher'
|
|
|
86
66
|
const emit = log ?? ((line) => runtimeLog.warn(label, line));
|
|
87
67
|
const announced = new Set();
|
|
88
68
|
let pendingAgents = [];
|
|
89
|
-
let pendingItems = 0;
|
|
90
69
|
let flushScheduled = false;
|
|
91
70
|
// One envelope's entries are gated synchronously; a microtask-deferred
|
|
92
71
|
// flush turns a whole boot flood into a single summary line.
|
|
@@ -96,20 +75,21 @@ export function createStaleBacklogGate({ maxAgeMs, replayAll, label = 'launcher'
|
|
|
96
75
|
return;
|
|
97
76
|
const preview = pendingAgents.slice(0, 5).join(', ');
|
|
98
77
|
const more = pendingAgents.length > 5 ? `, +${pendingAgents.length - 5} more` : '';
|
|
99
|
-
emit(`startup backlog policy: skipped ${
|
|
78
|
+
emit(`startup backlog policy: skipped ${pendingAgents.length} agent(s) whose newest mail is older than ${formatAge(maxAgeMs)} (${preview}${more}) — set FLEET_REPLAY_STALE=1 to replay them`);
|
|
100
79
|
pendingAgents = [];
|
|
101
|
-
pendingItems = 0;
|
|
102
80
|
};
|
|
103
81
|
return (entry) => {
|
|
104
82
|
if (replayAll)
|
|
105
83
|
return true;
|
|
106
|
-
|
|
84
|
+
// Open work always starts, however old: the task is unfinished, not stale.
|
|
85
|
+
if (entry.hasOpenTasks)
|
|
86
|
+
return true;
|
|
87
|
+
const newest = newestActionableAt(entry);
|
|
107
88
|
if (newest == null || now() - newest <= maxAgeMs)
|
|
108
89
|
return true;
|
|
109
90
|
if (!announced.has(entry.agentId)) {
|
|
110
91
|
announced.add(entry.agentId);
|
|
111
92
|
pendingAgents.push(entry.agentId);
|
|
112
|
-
pendingItems += countItems(entry.inbox);
|
|
113
93
|
if (!flushScheduled) {
|
|
114
94
|
flushScheduled = true;
|
|
115
95
|
queueMicrotask(flush);
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
* its `tools:` array. Each tool reads operatorKey/agentId from tool context and
|
|
16
16
|
* wraps a backend HTTP client — no per-agent plumbing.
|
|
17
17
|
*/
|
|
18
|
-
export { PROTOCOL_TOOLS, agreementProposeTool, agreementSubcontractTool, agreementRespondTool,
|
|
18
|
+
export { PROTOCOL_TOOLS, agreementProposeTool, agreementSubcontractTool, agreementRespondTool, agreementClaimTool, agreementCounterTool, agreementGetTool, agreementRevokeTool, agreementFulfillTool, taskCreateTool, taskSetResultTool, taskUpdatePlanStepTool, } from './tier1/protocolTools.js';
|
|
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';
|
|
@@ -23,6 +23,6 @@ export { PAYMENT_TOOLS, paymentBalanceTool, paymentTransferTool, paymentWaitForA
|
|
|
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 { LINK_TOOLS,
|
|
26
|
+
export { LINK_TOOLS, linkCreateInviteTool, linkListTool, } from './tier2/linkTools.js';
|
|
27
27
|
export { ARTIFACT_TOOLS, recordArtifactTool } from './tier2/artifactTools.js';
|
|
28
28
|
export { CHAT_TOOLS, openConversationTool } from './tier2/chatTools.js';
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
* wraps a backend HTTP client — no per-agent plumbing.
|
|
17
17
|
*/
|
|
18
18
|
// ── Tier 1: protocol grammar ────────────────────────────────────────────────
|
|
19
|
-
export { PROTOCOL_TOOLS, agreementProposeTool, agreementSubcontractTool, agreementRespondTool,
|
|
19
|
+
export { PROTOCOL_TOOLS, agreementProposeTool, agreementSubcontractTool, agreementRespondTool, agreementClaimTool, agreementCounterTool, agreementGetTool, agreementRevokeTool, agreementFulfillTool, taskCreateTool, taskSetResultTool, taskUpdatePlanStepTool, } from './tier1/protocolTools.js';
|
|
20
20
|
export { dispatchProtocolOp } from './tier1/protocolRunner.js';
|
|
21
21
|
// ── Tier 2: capability bundles ──────────────────────────────────────────────
|
|
22
22
|
export { DISCOVERY_TOOLS, agentSearchTool, agentGetTool } from './tier2/discoveryTools.js';
|
|
@@ -28,9 +28,11 @@ export { CONTEXT_TOOLS, contextReadTool, contextDelegateTool, contextExpandReach
|
|
|
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
30
|
// ZIG-898: links (agreements with engagementKind 'link') — cross-org reach.
|
|
31
|
-
|
|
31
|
+
// ZIG-1021/1022: the rail is two tools; propose/claim/revoke ride the
|
|
32
|
+
// agreement grammar.
|
|
33
|
+
export { LINK_TOOLS, linkCreateInviteTool, linkListTool, } from './tier2/linkTools.js';
|
|
32
34
|
// ZIG-899: deliberate artifact recording (the model decides "record this").
|
|
33
35
|
export { ARTIFACT_TOOLS, recordArtifactTool } from './tier2/artifactTools.js';
|
|
34
|
-
// ZIG-900 decision:
|
|
35
|
-
// read (
|
|
36
|
+
// ZIG-900 decision: chat_open only — chat listing stays a context-rail
|
|
37
|
+
// read (grant_list scopeKind=chat), no duplicate lister (ZIG-626 rule).
|
|
36
38
|
export { CHAT_TOOLS, openConversationTool } from './tier2/chatTools.js';
|
|
@@ -30,7 +30,7 @@ interface ProtocolPayload {
|
|
|
30
30
|
plan?: unknown;
|
|
31
31
|
planReviewTiming?: string;
|
|
32
32
|
requireMidWorkPlanAck?: boolean;
|
|
33
|
-
engagementKind?: 'hire' | 'service';
|
|
33
|
+
engagementKind?: 'hire' | 'service' | 'link';
|
|
34
34
|
subtasks?: (string | {
|
|
35
35
|
description?: string;
|
|
36
36
|
})[];
|
|
@@ -50,6 +50,11 @@ export async function dispatchProtocolOp(services, payload, chatId, agents = [],
|
|
|
50
50
|
if (!payload.agreementId || !payload.action)
|
|
51
51
|
return null;
|
|
52
52
|
return agreementService.respond(payload.agreementId, payload.action);
|
|
53
|
+
case 'agreement-claim':
|
|
54
|
+
// ZIG-1021: the one claim verb — quest, standing offer, or link invite.
|
|
55
|
+
if (!payload.agreementId)
|
|
56
|
+
throw new Error('agreement-claim requires `agreementId`');
|
|
57
|
+
return agreementService.claim(payload.agreementId);
|
|
53
58
|
case 'agreement-counter-proposal':
|
|
54
59
|
if (!payload.agreementId)
|
|
55
60
|
return null;
|
|
@@ -192,13 +197,36 @@ async function proposeAgreement(services, payload, chatId, _agents, actingAgentI
|
|
|
192
197
|
if (!payload.description)
|
|
193
198
|
throw new Error('agreement-propose requires `description`');
|
|
194
199
|
if (!payload.proposedTo)
|
|
195
|
-
throw new Error('agreement-propose requires `proposedTo` (userId or agentId). To
|
|
200
|
+
throw new Error('agreement-propose requires `proposedTo` (userId or agentId). To create a task under an existing agreement, use `task_create` instead.');
|
|
196
201
|
const isBroadcast = payload.proposedTo === 'everyone' || payload.proposedTo === 'org';
|
|
202
|
+
// ZIG-1022: a link proposal is an agreement with engagementKind 'link' —
|
|
203
|
+
// no chat, no money; the target owner approves. Folds the old request_link.
|
|
204
|
+
if (payload.engagementKind === 'link') {
|
|
205
|
+
if (isBroadcast) {
|
|
206
|
+
throw new Error('A broadcast link is an invite — use link_create_invite instead; propose links to one agent id.');
|
|
207
|
+
}
|
|
208
|
+
return services.agreementService.requestLink(payload.proposedTo, payload.description);
|
|
209
|
+
}
|
|
197
210
|
if (!isBroadcast && !payload.providerId) {
|
|
198
211
|
throw new Error('agreement-propose requires `providerId` — name who does the work: your own id (you are offering to work) or the proposedTo id (you are commissioning the recipient).');
|
|
199
212
|
}
|
|
200
213
|
if (isBroadcast && payload.providerId) {
|
|
201
|
-
|
|
214
|
+
// ZIG-1022: providerId = self on a broadcast publishes a STANDING OFFER
|
|
215
|
+
// (seller side open to any claimer). Any other providerId stays an error.
|
|
216
|
+
if (actingAgentId && payload.providerId === actingAgentId) {
|
|
217
|
+
const priced = applyFleetPricingForProtocol('agreement-propose', payload, actingAgentId);
|
|
218
|
+
return services.agreementService.publishOffer({
|
|
219
|
+
description: String(priced.description ?? ''),
|
|
220
|
+
price: typeof priced.price === 'number' ? priced.price : undefined,
|
|
221
|
+
lifecycle: typeof priced.lifecycle === 'string' ? priced.lifecycle : undefined,
|
|
222
|
+
expiresAt: typeof priced.expiresAt === 'string' ? priced.expiresAt : undefined,
|
|
223
|
+
maxExecutions: typeof priced.maxExecutions === 'number' ? priced.maxExecutions : undefined,
|
|
224
|
+
engagementKind: priced.engagementKind === 'hire' ? 'hire' : priced.engagementKind === 'service' ? 'service' : undefined,
|
|
225
|
+
billing: priced['billing'] === 'per_task' ? 'per_task' : priced['billing'] === 'total' ? 'total' : undefined,
|
|
226
|
+
audience: payload.proposedTo === 'org' ? 'org' : 'everyone',
|
|
227
|
+
});
|
|
228
|
+
}
|
|
229
|
+
throw new Error('On a broadcast, providerId must be your own agent id (a standing offer) or omitted (a quest — the claiming agent fills the open side).');
|
|
202
230
|
}
|
|
203
231
|
const priced = applyFleetPricingForProtocol('agreement-propose', payload, actingAgentId);
|
|
204
232
|
const { operation: _op, ...fields } = priced;
|
|
@@ -2,11 +2,12 @@ import { type ToolDefinition } from '../../../tools/defineTool.js';
|
|
|
2
2
|
export declare const agreementProposeTool: ToolDefinition;
|
|
3
3
|
export declare const agreementSubcontractTool: ToolDefinition;
|
|
4
4
|
export declare const agreementRespondTool: ToolDefinition;
|
|
5
|
-
export declare const
|
|
6
|
-
export declare const
|
|
7
|
-
export declare const
|
|
5
|
+
export declare const agreementClaimTool: ToolDefinition;
|
|
6
|
+
export declare const taskSetResultTool: ToolDefinition;
|
|
7
|
+
export declare const agreementCounterTool: ToolDefinition;
|
|
8
|
+
export declare const agreementGetTool: ToolDefinition;
|
|
8
9
|
export declare const agreementRevokeTool: ToolDefinition;
|
|
9
10
|
export declare const agreementFulfillTool: ToolDefinition;
|
|
10
11
|
export declare const taskUpdatePlanStepTool: ToolDefinition;
|
|
11
|
-
export declare const
|
|
12
|
+
export declare const taskCreateTool: ToolDefinition;
|
|
12
13
|
export declare const PROTOCOL_TOOLS: ToolDefinition[];
|