@ziggs-ai/agent-sdk 0.4.1 → 0.6.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 +21 -0
- 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 +47 -12
- package/dist/agent/claudeSdkTools.js +1 -1
- package/dist/defineAgent.d.ts +10 -5
- package/dist/defineAgent.js +16 -9
- package/dist/runtime/runTurn.js +12 -2
- 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/inboxLoop.d.ts +31 -11
- package/dist/server/inboxLoop.js +134 -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/tier1/protocolTools.js +2 -2
- package/dist/server/tools/tier2/artifactTools.d.ts +1 -1
- package/dist/server/tools/tier2/artifactTools.js +7 -82
- package/dist/server/tools/tier2/chatTools.d.ts +1 -1
- package/dist/server/tools/tier2/chatTools.js +7 -37
- package/dist/server/tools/tier2/connectionTools.d.ts +1 -1
- package/dist/server/tools/tier2/connectionTools.js +20 -94
- package/dist/server/tools/tier2/contextTools.d.ts +2 -2
- package/dist/server/tools/tier2/contextTools.js +10 -139
- package/dist/server/tools/tier2/discoveryTools.d.ts +1 -1
- package/dist/server/tools/tier2/discoveryTools.js +7 -24
- package/dist/server/tools/tier2/grantsTools.d.ts +1 -1
- package/dist/server/tools/tier2/grantsTools.js +6 -81
- package/dist/server/tools/tier2/linkTools.d.ts +1 -1
- package/dist/server/tools/tier2/linkTools.js +12 -156
- package/dist/server/tools/tier2/paymentTools.d.ts +2 -2
- package/dist/server/tools/tier2/paymentTools.js +16 -161
- package/dist/tools/capabilityAdapter.d.ts +11 -0
- package/dist/tools/capabilityAdapter.js +30 -0
- package/dist/types.d.ts +3 -2
- package/package.json +2 -2
package/dist/server/inboxLoop.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { ContextReadClient,
|
|
1
|
+
import { ContextReadClient, InboxClient, } 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,67 +7,61 @@ function log(label, msg) {
|
|
|
7
7
|
function warn(label, msg) {
|
|
8
8
|
runtimeLog.warn(label, msg);
|
|
9
9
|
}
|
|
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) {
|
|
10
|
+
/**
|
|
11
|
+
* Read the messages behind one chat's deliveries and hand them to the host.
|
|
12
|
+
*
|
|
13
|
+
* The envelope says which chat and how many, not the bodies — so this still
|
|
14
|
+
* reads through `/context/read`, where the agent's own grants fence what it is
|
|
15
|
+
* allowed to see. A delivery reference is an address, never an authorisation.
|
|
16
|
+
*/
|
|
17
|
+
async function deliverMessagesForChat(deps, news, earliestTs, reader) {
|
|
33
18
|
let delivered = 0;
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
const
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
via: `chat:${chatId}`,
|
|
19
|
+
let remaining = news.count;
|
|
20
|
+
// `after` is exclusive, so step back a millisecond to include the oldest
|
|
21
|
+
// delivery itself. Re-reading a neighbour that shares its timestamp is
|
|
22
|
+
// harmless — `seen` dedups by messageId.
|
|
23
|
+
let after = new Date(Date.parse(earliestTs) - 1).toISOString();
|
|
24
|
+
let pages = 0;
|
|
25
|
+
const maxPages = 20;
|
|
26
|
+
while (remaining > 0 && pages < maxPages) {
|
|
27
|
+
const limit = Math.min(READ_PAGE, remaining);
|
|
28
|
+
let page;
|
|
29
|
+
try {
|
|
30
|
+
page = await reader.read('messages', {
|
|
31
|
+
via: `chat:${news.chatId}`,
|
|
47
32
|
after,
|
|
48
33
|
direction: 'forward',
|
|
49
34
|
limit,
|
|
50
35
|
});
|
|
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
36
|
}
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
37
|
+
catch (err) {
|
|
38
|
+
const status = err.status;
|
|
39
|
+
if (status !== 403 && status !== 404)
|
|
40
|
+
throw err;
|
|
41
|
+
// Addressed but not readable. Since the delivery log and the grants are
|
|
42
|
+
// now separate, the two can disagree — a grant revoked between the write
|
|
43
|
+
// and this read, or a chat since deleted. Skipping keeps the pass alive
|
|
44
|
+
// so the rest of the mail is delivered and the watermark still advances;
|
|
45
|
+
// failing here would wedge the agent on one unreadable item forever,
|
|
46
|
+
// re-woken by the same delivery on every pass.
|
|
47
|
+
warn(deps.label ?? 'inboxLoop', `chat ${news.chatId}: addressed but not readable (${status}) — skipping ${remaining} delivery/deliveries`);
|
|
48
|
+
return delivered;
|
|
49
|
+
}
|
|
50
|
+
for (const row of page.items) {
|
|
51
|
+
const messageId = String(row.messageId ?? '');
|
|
52
|
+
if (deps.seen.has(messageId))
|
|
53
|
+
continue;
|
|
54
|
+
const text = typeof row.text === 'string' ? row.text : '';
|
|
55
|
+
if (!text.trim())
|
|
56
|
+
continue;
|
|
57
|
+
await deps.onMessage(text, { ...row, chatId: news.chatId });
|
|
58
|
+
delivered++;
|
|
59
|
+
remaining--;
|
|
60
|
+
}
|
|
61
|
+
if (!page.hasMore || !page.latestSequence)
|
|
62
|
+
break;
|
|
63
|
+
after = page.latestSequence;
|
|
64
|
+
pages++;
|
|
71
65
|
}
|
|
72
66
|
return delivered;
|
|
73
67
|
}
|
|
@@ -81,51 +75,77 @@ export async function pollInboxOnce(deps, waitSeconds = 0) {
|
|
|
81
75
|
const label = deps.label ?? 'inboxLoop';
|
|
82
76
|
const inbox = new InboxClient(deps.creds.operatorKey, deps.creds.agentId, deps.baseUrl);
|
|
83
77
|
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
78
|
const envelope = await inbox.getInbox(waitSeconds > 0 ? { waitSeconds } : {});
|
|
86
|
-
const countCap = envelope.countCap;
|
|
87
79
|
let messagesDelivered = 0;
|
|
88
80
|
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)`);
|
|
81
|
+
let tasksDelivered = 0;
|
|
82
|
+
if (envelope.deliveriesCapped) {
|
|
83
|
+
// Informational only — does NOT block acking what WAS handed over. The
|
|
84
|
+
// tail stays unacked and arrives on the next pass; never acking here would
|
|
85
|
+
// make a badly backed-up agent replay its whole backlog forever.
|
|
86
|
+
warn(label, 'inbox deliveriesCapped (more mail than one envelope carries — the tail follows on the next pass)');
|
|
97
87
|
}
|
|
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
|
-
|
|
88
|
+
// Oldest delivery per chat: where to start reading that chat's messages.
|
|
89
|
+
const earliestByChat = new Map();
|
|
90
|
+
for (const d of envelope.deliveries) {
|
|
91
|
+
if (!d.chatId)
|
|
92
|
+
continue;
|
|
93
|
+
const seen = earliestByChat.get(d.chatId);
|
|
94
|
+
if (!seen || d.ts < seen)
|
|
95
|
+
earliestByChat.set(d.chatId, d.ts);
|
|
96
|
+
}
|
|
97
|
+
for (const news of envelope.chats) {
|
|
98
|
+
const earliest = earliestByChat.get(news.chatId) ?? news.latestAt;
|
|
99
|
+
const n = await deliverMessagesForChat(deps, news, earliest, reader);
|
|
100
|
+
messagesDelivered += n;
|
|
101
|
+
if (n > 0)
|
|
102
|
+
log(label, `chat ${news.chatId} delivered ${n} message(s)`);
|
|
103
|
+
}
|
|
104
|
+
// Everything that is not chat text is handed over as a resource event, from
|
|
105
|
+
// the delivery itself — the reference already carries the ids the host needs,
|
|
106
|
+
// so nothing has to be re-derived to name what changed.
|
|
107
|
+
for (const d of envelope.deliveries) {
|
|
108
|
+
if (d.kind === 'message')
|
|
109
|
+
continue;
|
|
110
|
+
const key = `delivery:${d.kind}:${d.resourceId}:${d.ts}`;
|
|
111
|
+
if (deps.seen.has(key))
|
|
112
|
+
continue;
|
|
113
|
+
deps.seen.mark(key);
|
|
114
|
+
await deps.onResourceEvent({
|
|
115
|
+
kind: d.kind,
|
|
116
|
+
ts: d.ts,
|
|
117
|
+
resourceId: d.resourceId,
|
|
118
|
+
...(d.chatId ? { chatId: d.chatId } : {}),
|
|
119
|
+
...(d.agreementId ? { agreementId: d.agreementId } : {}),
|
|
120
|
+
...(d.taskId ? { taskId: d.taskId } : {}),
|
|
121
|
+
change: 'created',
|
|
122
|
+
});
|
|
123
|
+
resourceEvents++;
|
|
124
|
+
}
|
|
125
|
+
// ZIG-973: the work channel. An open assigned task is redelivered on every
|
|
126
|
+
// pass until it closes (that IS the durable-wait contract), so dedup on
|
|
127
|
+
// taskId+updatedAt — a genuinely touched task wakes again, an untouched one
|
|
128
|
+
// doesn't storm the lane every long-poll.
|
|
129
|
+
for (const task of envelope.tasksAwaitingMe ?? []) {
|
|
130
|
+
const key = `task:${task.taskId}:${task.updatedAt ?? ''}`;
|
|
131
|
+
if (deps.seen.has(key))
|
|
132
|
+
continue;
|
|
133
|
+
deps.seen.mark(key);
|
|
134
|
+
if (deps.onTask) {
|
|
135
|
+
await deps.onTask(task);
|
|
136
|
+
tasksDelivered++;
|
|
126
137
|
}
|
|
127
138
|
}
|
|
139
|
+
if (envelope.truncatedTasks) {
|
|
140
|
+
warn(label, `inbox truncatedTasks=${envelope.truncatedTasks} (more assigned tasks than one pass surfaces — they appear as earlier ones close)`);
|
|
141
|
+
}
|
|
142
|
+
// Pending decisions are standing state, not deliveries: they keep appearing
|
|
143
|
+
// until answered, so dedup them the same way tasks are deduped.
|
|
128
144
|
for (const proposal of envelope.proposalsAwaitingMe) {
|
|
145
|
+
const key = `proposal:${proposal.agreementId}`;
|
|
146
|
+
if (deps.seen.has(key))
|
|
147
|
+
continue;
|
|
148
|
+
deps.seen.mark(key);
|
|
129
149
|
await deps.onResourceEvent({
|
|
130
150
|
kind: 'agreement',
|
|
131
151
|
ts: proposal.proposedAt ?? new Date().toISOString(),
|
|
@@ -136,18 +156,21 @@ export async function pollInboxOnce(deps, waitSeconds = 0) {
|
|
|
136
156
|
});
|
|
137
157
|
resourceEvents++;
|
|
138
158
|
}
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
159
|
+
// Act, then ack: everything above has been handed to the host, so the
|
|
160
|
+
// watermark may now move past it. A crash before this point redelivers.
|
|
161
|
+
let acked = false;
|
|
162
|
+
if (envelope.ackTo) {
|
|
163
|
+
await inbox.ack(envelope.ackTo);
|
|
164
|
+
acked = true;
|
|
165
|
+
log(label, `acked up to ${envelope.ackTo}`);
|
|
144
166
|
}
|
|
145
167
|
return {
|
|
146
|
-
|
|
168
|
+
deliveries: envelope.deliveries.length,
|
|
147
169
|
messagesDelivered,
|
|
148
170
|
resourceEvents,
|
|
171
|
+
tasksDelivered,
|
|
149
172
|
acked,
|
|
150
|
-
truncated: envelope.
|
|
173
|
+
truncated: envelope.deliveriesCapped,
|
|
151
174
|
};
|
|
152
175
|
}
|
|
153
176
|
const DEFAULT_WAIT_SECONDS = 25;
|
|
@@ -160,18 +183,25 @@ function sleep(ms) {
|
|
|
160
183
|
* Run {@link pollInboxOnce} forever (until stopped). Each iteration long-polls
|
|
161
184
|
* for `waitSeconds`; a passing envelope returns instantly, an empty one holds.
|
|
162
185
|
* Errors retry with exponential backoff + jitter and never exit the loop —
|
|
163
|
-
* there is nothing to resynchronize (the
|
|
164
|
-
* just the next iteration.
|
|
186
|
+
* there is nothing to resynchronize (the watermark is server-side), so recovery
|
|
187
|
+
* is just the next iteration.
|
|
188
|
+
*
|
|
189
|
+
* The FIRST pass is an immediate read. A held poll only answers to new mail, so
|
|
190
|
+
* a host that just started (or restarted after a crash) would otherwise sit
|
|
191
|
+
* waiting while the work it was already assigned — open tasks, pending
|
|
192
|
+
* decisions — went unseen until something unrelated arrived.
|
|
165
193
|
*/
|
|
166
194
|
export function runInboxLoop(deps, opts = {}) {
|
|
167
195
|
const label = deps.label ?? 'inboxLoop';
|
|
168
196
|
const waitSeconds = opts.waitSeconds ?? DEFAULT_WAIT_SECONDS;
|
|
169
197
|
let running = true;
|
|
170
198
|
let consecutiveErrors = 0;
|
|
199
|
+
let first = true;
|
|
171
200
|
const done = (async () => {
|
|
172
201
|
while (running) {
|
|
173
202
|
try {
|
|
174
|
-
await pollInboxOnce(deps, waitSeconds);
|
|
203
|
+
await pollInboxOnce(deps, first ? 0 : waitSeconds);
|
|
204
|
+
first = false;
|
|
175
205
|
consecutiveErrors = 0;
|
|
176
206
|
}
|
|
177
207
|
catch (err) {
|
|
@@ -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);
|
|
@@ -19,14 +19,14 @@ function handlerFor(operation) {
|
|
|
19
19
|
return result;
|
|
20
20
|
};
|
|
21
21
|
}
|
|
22
|
-
export const agreementProposeTool = defineTool('agreement_propose', { description: { type: 'string', required: true }, proposedTo: { type: 'string', required: true }, engagementKind: { type: 'string', enum: ['hire', 'service'], description: 'Contract type. Defaults to service when omitted.' }, parentAgreementId: 'string', parentTaskId: 'string', providerId: { type: 'string', description: 'Who does the work — required on direct proposals, forbidden on broadcasts (the claiming agent fills the open side). Pass your own id when YOU are offering to do the work, or the proposedTo id when you are commissioning the RECIPIENT. Naming any other third party requires that party to have a matching published offer. The payer is always derived as the non-providing side.' }, price: 'number', lifecycle: 'string', expiresAt: 'string', maxExecutions: 'number', agreementDescription: 'string', plan: { type: 'object' }, planReviewTiming: 'string', requireMidWorkPlanAck: 'boolean' }, handlerFor('agreement-propose'), { description: 'Propose a contract to a user, agent, or as an open broadcast. Direction is stated, never guessed: providerId = your own id ("I work, you pay") or providerId = proposedTo ("you work, I pay"). Required on direct proposals; omit it only for a broadcast — proposedTo="everyone" (fully public — any agent may claim) or proposedTo="org" (scoped to your org). engagementKind defaults to service.', isAgreementCreation: true });
|
|
22
|
+
export const agreementProposeTool = defineTool('agreement_propose', { description: { type: 'string', required: true }, proposedTo: { type: 'string', required: true }, engagementKind: { type: 'string', enum: ['hire', 'service'], description: 'Contract type. Defaults to service when omitted.' }, parentAgreementId: 'string', parentTaskId: 'string', providerId: { type: 'string', description: 'Who does the work — required on direct proposals, forbidden on broadcasts (the claiming agent fills the open side). Pass your own id when YOU are offering to do the work, or the proposedTo id when you are commissioning the RECIPIENT. Naming any other third party requires that party to have a matching published offer. The payer is always derived as the non-providing side.' }, price: 'number', lifecycle: 'string', expiresAt: 'string', maxExecutions: 'number', billing: { type: 'string', enum: ['total', 'per_task'], description: "How price reads: 'total' (default, escrowed and paid at the end) or 'per_task' (a rate settled as each task completes — standing agreements only, and the default for a hire)." }, agreementDescription: 'string', plan: { type: 'object' }, planReviewTiming: 'string', requireMidWorkPlanAck: 'boolean' }, handlerFor('agreement-propose'), { description: 'Propose a contract to a user, agent, or as an open broadcast. Direction is stated, never guessed: providerId = your own id ("I work, you pay") or providerId = proposedTo ("you work, I pay"). Required on direct proposals; omit it only for a broadcast — proposedTo="everyone" (fully public — any agent may claim) or proposedTo="org" (scoped to your org). engagementKind defaults to service. Agreements are STANDING by default (no expiry, unlimited tasks under one agreement); set expiresAt or maxExecutions only when the engagement should end on its own.', isAgreementCreation: true });
|
|
23
23
|
export const agreementSubcontractTool = defineTool('agreement_subcontract', { description: { type: 'string', required: true }, executorId: { type: 'string', required: true }, parentAgreementId: { type: 'string', required: true }, parentTaskId: 'string', payerId: 'string', price: 'number', lifecycle: 'string', expiresAt: 'string', maxExecutions: 'number', agreementDescription: 'string', plan: { type: 'object' }, planReviewTiming: 'string', requireMidWorkPlanAck: 'boolean' }, handlerFor('agreement-subcontract'), { description: 'Delegate work to another agent under an existing parent agreement. Requires parentAgreementId.', isAgreementCreation: true });
|
|
24
24
|
export const agreementRespondTool = defineTool('agreement_respond', { agreementId: { type: 'string', required: true }, action: { type: 'string', required: true, enum: ['approve', 'reject'] } }, handlerFor('agreement-respond'), { description: 'Approve or reject a pending agreement proposal.' });
|
|
25
25
|
export const taskUpdateTool = defineTool('task_update', { taskId: { type: 'string', required: true }, status: { type: 'string', required: true, enum: ['completed', 'failed', 'cancelled'] }, result: 'string' }, handlerFor('task-update'), { description: 'Transition a task to completed, failed, or cancelled. Include a result string for completed tasks.' });
|
|
26
26
|
export const agreementCounterProposalTool = defineTool('agreement_counter_proposal', { agreementId: { type: 'string', required: true }, price: 'number', agreementDescription: 'string', expiresAt: 'string', lifecycle: 'string', maxExecutions: 'number', description: 'string', plan: { type: 'object' }, planReviewTiming: 'string', requireMidWorkPlanAck: 'boolean' }, handlerFor('agreement-counter-proposal'), { description: 'Counter a pending proposal with revised terms instead of approving or rejecting. Check status first with agreement_check_proposal.' });
|
|
27
27
|
export const agreementCheckProposalTool = defineTool('agreement_check_proposal', { agreementId: { type: 'string', required: true } }, handlerFor('agreement-check-proposal'), { description: 'Read the current proposal status for an agreement (pending, approved, rejected, countered, expired).' });
|
|
28
28
|
export const agreementRevokeTool = defineTool('agreement_revoke', { agreementId: { type: 'string', required: true } }, handlerFor('agreement-revoke'), { description: 'Revoke a pending proposal you created so it can no longer be approved. Use when the user has clearly moved on to a different request before responding to the previous proposal card.' });
|
|
29
|
-
export const agreementFulfillTool = defineTool('agreement_fulfill', { agreementId: { type: 'string', required: true } }, handlerFor('agreement-fulfill'), { description: '
|
|
29
|
+
export const agreementFulfillTool = defineTool('agreement_fulfill', { agreementId: { type: 'string', required: true } }, handlerFor('agreement-fulfill'), { description: 'END an agreement you provide — permanently. Fulfilling terminates the whole relationship, not one deliverable: every grant it conferred is revoked, its shared space is torn down, and it cannot be reopened. Finished WORK is reported via task_update (the task closes; the agreement stays standing for the next task). Only fulfill when the engagement\'s full scope is delivered and nothing more is expected — never a standing hire that just finished a task.' });
|
|
30
30
|
export const taskUpdatePlanStepTool = defineTool('task_update_plan_step', { taskId: { type: 'string', required: true }, stepId: { type: 'string', required: true }, status: { type: 'string', required: true }, result: 'string' }, handlerFor('task-update-plan-step'), { description: "Update the status of a single step within a task's plan." });
|
|
31
31
|
export const taskSpawnTool = defineTool('task_spawn', { agreementId: { type: 'string', required: true }, description: { type: 'string', required: true }, parentTaskId: 'string', assigneeId: { type: 'string', description: 'Explicitly hand this task to one specific agent (e.g. a worker your orchestrator selected via agent search) instead of leaving it unassigned. Must be a party on the agreement (payer, provider, creator, or creator agent).' }, inputArtifactIds: { type: 'array', items: { type: 'string' }, description: 'ZIG-734: prior-step output artifact handles for structured input links.' }, plan: { type: 'object' }, planReviewTiming: 'string', requireMidWorkPlanAck: 'boolean' }, handlerFor('task-spawn'), { description: 'Spawn a new task under an existing approved agreement. Use when the contract is already in place. Pass assigneeId to delegate the task to a specific agent rather than leaving it open.' });
|
|
32
32
|
export const PROTOCOL_TOOLS = [
|
|
@@ -1,82 +1,7 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
* delegate protocol tells agents to do for heavy results. Rides
|
|
9
|
-
* ArtifactsClient.writeStrict (fail loudly, return the artifactId) — the same
|
|
10
|
-
* wire call as the MCP tool.
|
|
11
|
-
*/
|
|
12
|
-
function creds(ctx) {
|
|
13
|
-
const operatorKey = ctx?.['operatorKey'];
|
|
14
|
-
const agentId = ctx?.['agentId'];
|
|
15
|
-
if (!operatorKey)
|
|
16
|
-
throw new Error('operatorKey missing from tool context');
|
|
17
|
-
if (!agentId)
|
|
18
|
-
throw new Error('agentId missing from tool context');
|
|
19
|
-
return { operatorKey, agentId };
|
|
20
|
-
}
|
|
21
|
-
/**
|
|
22
|
-
* ZIG-560 teaching carried over from the MCP tool: name the result slot on the
|
|
23
|
-
* success path so an agent finds the right move unaided — reworded to the SDK
|
|
24
|
-
* task grammar (task_update carries the terminal result on this surface).
|
|
25
|
-
*/
|
|
26
|
-
function reportingHint(contentType, taskId) {
|
|
27
|
-
if (contentType === 'result') {
|
|
28
|
-
return taskId
|
|
29
|
-
? 'Recorded as a task-bound result artifact. Close the task by setting its terminal result with task_update.'
|
|
30
|
-
: 'Recorded as a result artifact, but not bound to a task — pass taskId to bind it, then close the task with task_update.';
|
|
31
|
-
}
|
|
32
|
-
return 'Reporting finished work? Record it with content_type=result bound to the task (taskId), then close the task with task_update — chat messages are conversation only.';
|
|
33
|
-
}
|
|
34
|
-
export const recordArtifactTool = defineTool('record_artifact', {
|
|
35
|
-
text: { type: 'string', required: true, description: 'Artifact body' },
|
|
36
|
-
visibility: {
|
|
37
|
-
type: 'string',
|
|
38
|
-
required: true,
|
|
39
|
-
enum: ['chat', 'agent-private'],
|
|
40
|
-
description: 'chat = visible to scope parties; agent-private = your eyes only',
|
|
41
|
-
},
|
|
42
|
-
chatId: { type: 'string', description: 'Target chat (xor agreementId)' },
|
|
43
|
-
agreementId: { type: 'string', description: 'Target agreement (xor chatId)' },
|
|
44
|
-
taskId: {
|
|
45
|
-
type: 'string',
|
|
46
|
-
description: 'Optional task — creates a TaskArtifactLink alongside the primary scope link',
|
|
47
|
-
},
|
|
48
|
-
content_type: { type: 'string', description: 'Default text; use result for a finished deliverable' },
|
|
49
|
-
}, async (args, ctx) => {
|
|
50
|
-
const chatId = args['chatId'];
|
|
51
|
-
const agreementId = args['agreementId'];
|
|
52
|
-
if ((chatId && agreementId) || (!chatId && !agreementId)) {
|
|
53
|
-
throw new Error('Pass exactly one of chatId or agreementId');
|
|
54
|
-
}
|
|
55
|
-
const visibility = args['visibility'];
|
|
56
|
-
if (visibility !== 'chat' && visibility !== 'agent-private') {
|
|
57
|
-
throw new Error('visibility must be chat or agent-private');
|
|
58
|
-
}
|
|
59
|
-
const contentType = args['content_type'];
|
|
60
|
-
const taskId = args['taskId'];
|
|
61
|
-
const { operatorKey, agentId } = creds(ctx);
|
|
62
|
-
const { artifactId } = await new ArtifactsClient(operatorKey, agentId).writeStrict({
|
|
63
|
-
text: args['text'],
|
|
64
|
-
visibility,
|
|
65
|
-
chatId,
|
|
66
|
-
agreementId,
|
|
67
|
-
taskId,
|
|
68
|
-
content_type: contentType,
|
|
69
|
-
});
|
|
70
|
-
return {
|
|
71
|
-
ok: true,
|
|
72
|
-
artifactId,
|
|
73
|
-
visibility,
|
|
74
|
-
chatId,
|
|
75
|
-
agreementId,
|
|
76
|
-
taskId,
|
|
77
|
-
reportingHint: reportingHint(contentType, taskId),
|
|
78
|
-
};
|
|
79
|
-
}, {
|
|
80
|
-
description: 'Write an artifact to a chat or agreement scope. Set visibility explicitly. For a finished deliverable, set content_type=result and pass taskId to bind it to the task — heavy results belong in artifacts, not chat messages.',
|
|
81
|
-
});
|
|
82
|
-
export const ARTIFACT_TOOLS = [recordArtifactTool];
|
|
1
|
+
import { ARTIFACT_CAPABILITIES, recordArtifactCapability } from '@ziggs-ai/api-client';
|
|
2
|
+
import { toolFromCapability, toolsFromCapabilities } from '../../../tools/capabilityAdapter.js';
|
|
3
|
+
// ZIG-899 / ZIG-956 — deliberate artifact recording, defined once in
|
|
4
|
+
// api-client's shared capability layer (including the ZIG-560 result-slot
|
|
5
|
+
// teaching, worded per surface task grammar).
|
|
6
|
+
export const recordArtifactTool = toolFromCapability(recordArtifactCapability);
|
|
7
|
+
export const ARTIFACT_TOOLS = toolsFromCapabilities(ARTIFACT_CAPABILITIES);
|
|
@@ -1,37 +1,7 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
* "what chats can I see" is already answered by list_grants scopeKind=chat —
|
|
9
|
-
* adding a lister would duplicate a context-family read (ZIG-626 rule).
|
|
10
|
-
*/
|
|
11
|
-
function creds(ctx) {
|
|
12
|
-
const operatorKey = ctx?.['operatorKey'];
|
|
13
|
-
const agentId = ctx?.['agentId'];
|
|
14
|
-
if (!operatorKey)
|
|
15
|
-
throw new Error('operatorKey missing from tool context');
|
|
16
|
-
if (!agentId)
|
|
17
|
-
throw new Error('agentId missing from tool context');
|
|
18
|
-
return { operatorKey, agentId };
|
|
19
|
-
}
|
|
20
|
-
export const openConversationTool = defineTool('open_conversation', {
|
|
21
|
-
participantId: {
|
|
22
|
-
type: 'string',
|
|
23
|
-
required: true,
|
|
24
|
-
description: 'User or agent id to converse with (from agent_search — do not guess ids)',
|
|
25
|
-
},
|
|
26
|
-
}, async (args, ctx) => {
|
|
27
|
-
if (!args['participantId'])
|
|
28
|
-
throw new Error('participantId is required');
|
|
29
|
-
const { chatId } = await openConversation(args['participantId'], creds(ctx));
|
|
30
|
-
return {
|
|
31
|
-
chatId,
|
|
32
|
-
note: 'Conversation is open (or reused). Your membership auto-mints a chat grant, so the chat also appears in list_grants scopeKind=chat for context reads.',
|
|
33
|
-
};
|
|
34
|
-
}, {
|
|
35
|
-
description: 'Open or reuse a chat with a user or agent participant. To reach an agent in ANOTHER org, establish a link first — request_link (if you have its agent id) or create_link_invite (if you do not), approved/claimed — otherwise this fails with AGENT_NOT_PUBLISHED. To list chats you can already read, use list_grants scopeKind=chat.',
|
|
36
|
-
});
|
|
37
|
-
export const CHAT_TOOLS = [openConversationTool];
|
|
1
|
+
import { CHAT_CAPABILITIES, openConversationCapability } from '@ziggs-ai/api-client';
|
|
2
|
+
import { toolFromCapability, toolsFromCapabilities } from '../../../tools/capabilityAdapter.js';
|
|
3
|
+
// ZIG-900 / ZIG-956 — open_conversation only (chat listing stays a
|
|
4
|
+
// context-rail read, ZIG-626 rule); defined once in api-client's shared
|
|
5
|
+
// capability layer.
|
|
6
|
+
export const openConversationTool = toolFromCapability(openConversationCapability);
|
|
7
|
+
export const CHAT_TOOLS = toolsFromCapabilities(CHAT_CAPABILITIES);
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { type ToolDefinition } from '../../../tools/defineTool.js';
|
|
2
2
|
export declare const connectionProxyTool: ToolDefinition;
|
|
3
|
-
export declare const connectionListGrantsTool: ToolDefinition;
|
|
4
3
|
export declare const requestConnectionTool: ToolDefinition;
|
|
4
|
+
export declare const connectionListGrantsTool: ToolDefinition;
|
|
5
5
|
export declare const CONNECTION_TOOLS: ToolDefinition[];
|