@ziggs-ai/agent-sdk 0.16.1 → 0.17.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/CHANGELOG.md +10 -0
- package/README.md +110 -88
- package/dist/{AgentHost.d.ts → Agent.d.ts} +50 -27
- package/dist/{AgentHost.js → Agent.js} +135 -129
- package/dist/adapters/AnthropicAdapter.d.ts +1 -1
- package/dist/adapters/AnthropicAdapter.js +1 -1
- package/dist/{agent/ClaudeSdkAgent.d.ts → brain/ClaudeBrain.d.ts} +23 -11
- package/dist/{agent/ClaudeSdkAgent.js → brain/ClaudeBrain.js} +99 -99
- package/dist/{agent/Agent.d.ts → brain/ZiggsBrain.d.ts} +23 -20
- package/dist/{agent/Agent.js → brain/ZiggsBrain.js} +12 -11
- package/dist/{agent → brain}/claudeCognition.d.ts +1 -1
- package/dist/{agent → brain}/claudeCognition.js +1 -1
- package/dist/{agent → brain}/claudeSdkProtocol.d.ts +1 -1
- package/dist/{agent → brain}/claudeSdkProtocol.js +1 -1
- package/dist/{agent → brain}/claudeSdkTools.d.ts +1 -1
- package/dist/{agent → brain}/claudeSdkTools.js +5 -14
- package/dist/{agent → brain}/hostedAgentConnection.d.ts +1 -1
- package/dist/{agent → brain}/hostedAgentConnection.js +3 -3
- package/dist/brain/index.d.ts +6 -6
- package/dist/brain/index.js +2 -2
- package/dist/brain/onBehalfAgreement.d.ts +51 -0
- package/dist/brain/onBehalfAgreement.js +99 -0
- package/dist/{agent → brain}/toolManagerToSdkMcp.js +10 -1
- package/dist/defineAgent.js +4 -4
- package/dist/formatters/HistoryFormatter.js +2 -1
- package/dist/index.d.ts +30 -31
- package/dist/index.js +24 -25
- package/dist/{adapters/McpAdapter.d.ts → mcp/createMcpAgent.d.ts} +16 -15
- package/dist/{adapters/McpAdapter.js → mcp/createMcpAgent.js} +54 -40
- package/dist/memory/MemoryStore.d.ts +18 -1
- package/dist/memory/MemoryStore.js +43 -0
- package/dist/{server → platform}/ConnectionPool.d.ts +9 -3
- package/dist/{server → platform}/ConnectionPool.js +16 -4
- package/dist/{server → platform}/EventQueue.d.ts +1 -1
- package/dist/{server → platform}/StateTimers.d.ts +1 -1
- package/dist/{server → platform}/StateTimers.js +1 -1
- package/dist/{server → platform}/ZiggsEffectHandler.d.ts +4 -4
- package/dist/{server → platform}/ZiggsEffectHandler.js +2 -2
- package/dist/{server → platform}/inboxLoop.d.ts +34 -1
- package/dist/{server → platform}/inboxLoop.js +122 -14
- package/dist/{server → platform}/polls/PollKind.d.ts +4 -7
- package/dist/{server → platform}/polls/PollKind.js +3 -6
- package/dist/{server → platform}/polls/PollRunner.d.ts +5 -6
- package/dist/{server → platform}/polls/PollRunner.js +24 -12
- package/dist/platform/polls/declaredPollRunner.d.ts +4 -0
- package/dist/platform/polls/declaredPollRunner.js +29 -0
- package/dist/{server → platform}/tools/tier2/mcpConnectionTools.js +6 -3
- package/dist/runtime/bootstrapCtx.d.ts +1 -1
- package/dist/runtime/bootstrapCtx.js +1 -1
- package/dist/runtime/buildOutcome.js +1 -1
- package/dist/runtime/runTurn.d.ts +1 -1
- package/dist/runtime/runTurn.js +18 -18
- package/dist/runtime/validateWorkflow.js +1 -1
- package/dist/skills/parseSkill.d.ts +7 -2
- package/dist/skills/parseSkill.js +22 -5
- package/dist/skills/skillReadReference.d.ts +6 -0
- package/dist/skills/skillReadReference.js +27 -0
- package/dist/tools/needs.js +2 -2
- package/dist/tools/toolCreds.d.ts +1 -1
- package/dist/tools/toolCreds.js +1 -1
- package/dist/types.d.ts +3 -3
- package/package.json +2 -2
- package/dist/server/createHealthServer.d.ts +0 -7
- package/dist/server/createHealthServer.js +0 -75
- package/dist/server/runLauncher.d.ts +0 -22
- package/dist/server/runLauncher.js +0 -160
- /package/dist/{agent → brain}/toolManagerToSdkMcp.d.ts +0 -0
- /package/dist/{server → platform}/EventQueue.js +0 -0
- /package/dist/{server → platform}/OutboxBuffer.d.ts +0 -0
- /package/dist/{server → platform}/OutboxBuffer.js +0 -0
- /package/dist/{server → platform}/SeenMessages.d.ts +0 -0
- /package/dist/{server → platform}/SeenMessages.js +0 -0
- /package/dist/{server → platform}/agreements/AgreementService.d.ts +0 -0
- /package/dist/{server → platform}/agreements/AgreementService.js +0 -0
- /package/dist/{server → platform}/proactive/ProactiveTrigger.d.ts +0 -0
- /package/dist/{server → platform}/proactive/ProactiveTrigger.js +0 -0
- /package/dist/{server → platform}/requestMatch.d.ts +0 -0
- /package/dist/{server → platform}/requestMatch.js +0 -0
- /package/dist/{server → platform}/tasks/TaskService.d.ts +0 -0
- /package/dist/{server → platform}/tasks/TaskService.js +0 -0
- /package/dist/{server → platform}/tasks/index.d.ts +0 -0
- /package/dist/{server → platform}/tasks/index.js +0 -0
- /package/dist/{server → platform}/telemetryIngest.d.ts +0 -0
- /package/dist/{server → platform}/telemetryIngest.js +0 -0
- /package/dist/{server → platform}/tools/index.d.ts +0 -0
- /package/dist/{server → platform}/tools/index.js +0 -0
- /package/dist/{server → platform}/tools/tier1/protocolRunner.d.ts +0 -0
- /package/dist/{server → platform}/tools/tier1/protocolRunner.js +0 -0
- /package/dist/{server → platform}/tools/tier1/protocolTools.d.ts +0 -0
- /package/dist/{server → platform}/tools/tier1/protocolTools.js +0 -0
- /package/dist/{server → platform}/tools/tier2/artifactTools.d.ts +0 -0
- /package/dist/{server → platform}/tools/tier2/artifactTools.js +0 -0
- /package/dist/{server → platform}/tools/tier2/chatTools.d.ts +0 -0
- /package/dist/{server → platform}/tools/tier2/chatTools.js +0 -0
- /package/dist/{server → platform}/tools/tier2/connectionTools.d.ts +0 -0
- /package/dist/{server → platform}/tools/tier2/connectionTools.js +0 -0
- /package/dist/{server → platform}/tools/tier2/contextTools.d.ts +0 -0
- /package/dist/{server → platform}/tools/tier2/contextTools.js +0 -0
- /package/dist/{server → platform}/tools/tier2/discoveryTools.d.ts +0 -0
- /package/dist/{server → platform}/tools/tier2/discoveryTools.js +0 -0
- /package/dist/{server → platform}/tools/tier2/grantsTools.d.ts +0 -0
- /package/dist/{server → platform}/tools/tier2/grantsTools.js +0 -0
- /package/dist/{server → platform}/tools/tier2/introductionTools.d.ts +0 -0
- /package/dist/{server → platform}/tools/tier2/introductionTools.js +0 -0
- /package/dist/{server → platform}/tools/tier2/linkTools.d.ts +0 -0
- /package/dist/{server → platform}/tools/tier2/linkTools.js +0 -0
- /package/dist/{server → platform}/tools/tier2/marketplaceTools.d.ts +0 -0
- /package/dist/{server → platform}/tools/tier2/marketplaceTools.js +0 -0
- /package/dist/{server → platform}/tools/tier2/mcpConnectionTools.d.ts +0 -0
- /package/dist/{server → platform}/tools/tier2/paymentTools.d.ts +0 -0
- /package/dist/{server → platform}/tools/tier2/paymentTools.js +0 -0
- /package/dist/{server → platform}/tools/tier2/taskTools.d.ts +0 -0
- /package/dist/{server → platform}/tools/tier2/taskTools.js +0 -0
|
@@ -8,6 +8,62 @@ function log(label, msg) {
|
|
|
8
8
|
function warn(label, msg) {
|
|
9
9
|
runtimeLog.warn(label, msg);
|
|
10
10
|
}
|
|
11
|
+
/**
|
|
12
|
+
* Store publish waits on a `probe_*` chat reply, not inbox ack. Act those
|
|
13
|
+
* rooms before leftover mail so a standing host answers the gate even when
|
|
14
|
+
* the envelope also carries older chats.
|
|
15
|
+
*/
|
|
16
|
+
export function chatsInWakeOrder(chats) {
|
|
17
|
+
return [...chats].sort((a, b) => {
|
|
18
|
+
const ap = isPublishProbeChat(a.chatId) ? 0 : 1;
|
|
19
|
+
const bp = isPublishProbeChat(b.chatId) ? 0 : 1;
|
|
20
|
+
if (ap !== bp)
|
|
21
|
+
return ap - bp;
|
|
22
|
+
return a.latestAt < b.latestAt ? 1 : -1;
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
function isPublishProbeChat(chatId) {
|
|
26
|
+
return chatId.startsWith('probe_');
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Store publish waits on a chat reply, not inbox ack. The ordinary pass can
|
|
30
|
+
* already be inside leftover mail when that room appears, so a side read
|
|
31
|
+
* answers `probe_*` chats without waiting for the current pass to finish.
|
|
32
|
+
* It never acks — the main pass still owns the watermark.
|
|
33
|
+
*/
|
|
34
|
+
export async function answerPublishProbes(deps) {
|
|
35
|
+
const inbox = new InboxClient(deps.creds.operatorKey, deps.creds.agentId, deps.baseUrl);
|
|
36
|
+
const reader = new ContextReadClient(deps.creds.operatorKey, deps.creds.agentId, deps.baseUrl);
|
|
37
|
+
const envelope = await inbox.getInbox({});
|
|
38
|
+
const probes = (envelope.chats ?? []).filter((c) => isPublishProbeChat(c.chatId));
|
|
39
|
+
if (probes.length === 0)
|
|
40
|
+
return 0;
|
|
41
|
+
const mine = (d) => d.assigneeId === deps.ownAgentId;
|
|
42
|
+
const earliestByChat = new Map();
|
|
43
|
+
for (const d of envelope.deliveries) {
|
|
44
|
+
if (!d.chatId || !mine(d) || !isPublishProbeChat(d.chatId))
|
|
45
|
+
continue;
|
|
46
|
+
const seen = earliestByChat.get(d.chatId);
|
|
47
|
+
if (!seen || d.ts < seen)
|
|
48
|
+
earliestByChat.set(d.chatId, d.ts);
|
|
49
|
+
}
|
|
50
|
+
const attempt = async (what, handover) => {
|
|
51
|
+
try {
|
|
52
|
+
await handover();
|
|
53
|
+
return true;
|
|
54
|
+
}
|
|
55
|
+
catch (err) {
|
|
56
|
+
warn(deps.label ?? 'inboxLoop', `publish-probe ${what} was not handled: ${err?.message ?? err}`);
|
|
57
|
+
return false;
|
|
58
|
+
}
|
|
59
|
+
};
|
|
60
|
+
let delivered = 0;
|
|
61
|
+
for (const news of probes) {
|
|
62
|
+
const earliest = earliestByChat.get(news.chatId) ?? news.latestAt;
|
|
63
|
+
delivered += await deliverMessagesForChat(deps, news, earliest, reader, attempt);
|
|
64
|
+
}
|
|
65
|
+
return delivered;
|
|
66
|
+
}
|
|
11
67
|
/**
|
|
12
68
|
* Standing proposals share the merged inbox visibility model with deliveries,
|
|
13
69
|
* but older envelopes predate an explicit assignee stamp. Prefer the server's
|
|
@@ -116,9 +172,9 @@ export async function pollInboxOnce(deps, waitSeconds = 0, opts = {}) {
|
|
|
116
172
|
}
|
|
117
173
|
};
|
|
118
174
|
if (envelope.deliveriesCapped) {
|
|
119
|
-
//
|
|
120
|
-
// tail stays
|
|
121
|
-
//
|
|
175
|
+
// The listed window is the oldest CAP. Acking it slides the cursor; the
|
|
176
|
+
// newer tail stays past ackTo and arrives on the next pass. Holding here
|
|
177
|
+
// was the wedge: the same over-cap window came back forever.
|
|
122
178
|
warn(label, 'inbox deliveriesCapped (more mail than one envelope carries — the tail follows on the next pass)');
|
|
123
179
|
}
|
|
124
180
|
// The single-assign law: a row is MINE to act on iff it carries my stamp.
|
|
@@ -136,7 +192,7 @@ export async function pollInboxOnce(deps, waitSeconds = 0, opts = {}) {
|
|
|
136
192
|
if (!seen || d.ts < seen)
|
|
137
193
|
earliestByChat.set(d.chatId, d.ts);
|
|
138
194
|
}
|
|
139
|
-
for (const news of envelope.chats) {
|
|
195
|
+
for (const news of chatsInWakeOrder(envelope.chats ?? [])) {
|
|
140
196
|
const earliest = earliestByChat.get(news.chatId) ?? news.latestAt;
|
|
141
197
|
const n = await deliverMessagesForChat(deps, news, earliest, reader, attempt);
|
|
142
198
|
messagesDelivered += n;
|
|
@@ -195,7 +251,7 @@ export async function pollInboxOnce(deps, waitSeconds = 0, opts = {}) {
|
|
|
195
251
|
}
|
|
196
252
|
// request channel. Triage is the host's job (exact string compare);
|
|
197
253
|
// a miss must still count as handled so the watermark can advance with 0 tokens.
|
|
198
|
-
for (const request of envelope.
|
|
254
|
+
for (const request of envelope.openRequestsAwaitingMe ?? []) {
|
|
199
255
|
const key = `request:${request.agreementId}:${request.ts}`;
|
|
200
256
|
if (deps.seen.has(key))
|
|
201
257
|
continue;
|
|
@@ -245,12 +301,7 @@ export async function pollInboxOnce(deps, waitSeconds = 0, opts = {}) {
|
|
|
245
301
|
// this window has failed too many times in a row and is now poison, in which
|
|
246
302
|
// case it is dropped loudly rather than wedging the agent forever.
|
|
247
303
|
let acked = false;
|
|
248
|
-
|
|
249
|
-
// acking would refuse or bury them. Hold until a later uncapped pass.
|
|
250
|
-
if (envelope.deliveriesCapped && envelope.ackTo) {
|
|
251
|
-
warn(label, `holding the watermark at ${envelope.ackTo} — deliveriesCapped; ack would not cover the unlisted tail`);
|
|
252
|
-
}
|
|
253
|
-
else if (envelope.ackTo && (failures === 0 || opts.forceAck)) {
|
|
304
|
+
if (envelope.ackTo && (failures === 0 || opts.forceAck)) {
|
|
254
305
|
if (failures > 0) {
|
|
255
306
|
runtimeLog.error(label, `acking a window that failed ${failures} handover(s) — giving up on redelivery to keep the ` +
|
|
256
307
|
`agent alive; this mail is lost (ackTo=${envelope.ackTo})`);
|
|
@@ -261,7 +312,7 @@ export async function pollInboxOnce(deps, waitSeconds = 0, opts = {}) {
|
|
|
261
312
|
const handledResourceIds = [
|
|
262
313
|
...new Set([
|
|
263
314
|
...envelope.deliveries.filter(mine).map((d) => d.resourceId),
|
|
264
|
-
...(envelope.
|
|
315
|
+
...(envelope.openRequestsAwaitingMe ?? []).map((q) => q.agreementId),
|
|
265
316
|
]),
|
|
266
317
|
].filter((id) => typeof id === 'string' && id.length > 0);
|
|
267
318
|
await inbox.ack(envelope.ackTo, { handledResourceIds });
|
|
@@ -289,6 +340,16 @@ export async function pollInboxOnce(deps, waitSeconds = 0, opts = {}) {
|
|
|
289
340
|
* some defaults ship with. Lower it if your deployment sits behind a shorter one.
|
|
290
341
|
*/
|
|
291
342
|
const DEFAULT_WAIT_SECONDS = 110;
|
|
343
|
+
/**
|
|
344
|
+
* Side-poll interval for store-publish `probe_*` rooms.
|
|
345
|
+
*
|
|
346
|
+
* Default **off**. A second GET /inbox beside the main long-poll burned the
|
|
347
|
+
* shared poll ceiling: every hosted agent side-polled, probes got 429s, and
|
|
348
|
+
* Register saw ALB HTML 504s. Main pass + probe-first ordering + pinned
|
|
349
|
+
* `probe_*` deliveries are enough; set `probePollMs` only when you knowingly
|
|
350
|
+
* need the extra loop.
|
|
351
|
+
*/
|
|
352
|
+
const DEFAULT_PROBE_POLL_MS = 0;
|
|
292
353
|
const ERROR_BACKOFF_BASE_MS = 1_000;
|
|
293
354
|
const ERROR_BACKOFF_MAX_MS = 30_000;
|
|
294
355
|
/**
|
|
@@ -299,6 +360,14 @@ const ERROR_BACKOFF_MAX_MS = 30_000;
|
|
|
299
360
|
* rather than an unbounded loop.
|
|
300
361
|
*/
|
|
301
362
|
const MAX_HELD_PASSES = 3;
|
|
363
|
+
/**
|
|
364
|
+
* How many consecutive passes have refused to advance the watermark.
|
|
365
|
+
* A capped envelope used to reset the counter (zero handover failures),
|
|
366
|
+
* so `forceAck` never fired on the one hold that lasts forever.
|
|
367
|
+
*/
|
|
368
|
+
export function heldPassAfter(prev, pass) {
|
|
369
|
+
return !pass.acked && (pass.failures > 0 || pass.truncated) ? prev + 1 : 0;
|
|
370
|
+
}
|
|
302
371
|
function sleep(ms) {
|
|
303
372
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
304
373
|
}
|
|
@@ -317,21 +386,28 @@ function sleep(ms) {
|
|
|
317
386
|
export function runInboxLoop(deps, opts = {}) {
|
|
318
387
|
const label = deps.label ?? 'inboxLoop';
|
|
319
388
|
const waitSeconds = opts.waitSeconds ?? DEFAULT_WAIT_SECONDS;
|
|
389
|
+
const probePollMs = opts.probePollMs ?? DEFAULT_PROBE_POLL_MS;
|
|
320
390
|
let running = true;
|
|
321
391
|
let consecutiveErrors = 0;
|
|
322
392
|
let heldPasses = 0;
|
|
393
|
+
let lastPassAt = null;
|
|
394
|
+
let lastAttemptAt = null;
|
|
323
395
|
let first = true;
|
|
324
|
-
const
|
|
396
|
+
const main = (async () => {
|
|
325
397
|
while (running) {
|
|
326
398
|
try {
|
|
399
|
+
lastAttemptAt = Date.now();
|
|
327
400
|
const pass = await pollInboxOnce(deps, first ? 0 : waitSeconds, {
|
|
328
401
|
forceAck: heldPasses >= MAX_HELD_PASSES,
|
|
329
402
|
});
|
|
403
|
+
lastPassAt = Date.now();
|
|
330
404
|
first = false;
|
|
331
405
|
consecutiveErrors = 0;
|
|
332
406
|
// A held pass means the same window comes back; count how many times we
|
|
333
407
|
// have refused it so a permanently unhandleable frame cannot spin here.
|
|
334
|
-
|
|
408
|
+
// Capped holds used to reset this: failures === 0, so the poison
|
|
409
|
+
// escape never climbed. Count any pass that refused to advance.
|
|
410
|
+
heldPasses = heldPassAfter(heldPasses, pass);
|
|
335
411
|
}
|
|
336
412
|
catch (err) {
|
|
337
413
|
if (!running)
|
|
@@ -356,10 +432,42 @@ export function runInboxLoop(deps, opts = {}) {
|
|
|
356
432
|
}
|
|
357
433
|
}
|
|
358
434
|
})();
|
|
435
|
+
const probes = (async () => {
|
|
436
|
+
if (probePollMs <= 0)
|
|
437
|
+
return;
|
|
438
|
+
while (running) {
|
|
439
|
+
const slice = 50;
|
|
440
|
+
let left = probePollMs;
|
|
441
|
+
while (left > 0 && running) {
|
|
442
|
+
const step = Math.min(slice, left);
|
|
443
|
+
await sleep(step);
|
|
444
|
+
left -= step;
|
|
445
|
+
}
|
|
446
|
+
if (!running)
|
|
447
|
+
break;
|
|
448
|
+
try {
|
|
449
|
+
const n = await answerPublishProbes(deps);
|
|
450
|
+
if (n > 0)
|
|
451
|
+
log(label, `publish-probe side poll delivered ${n} message(s)`);
|
|
452
|
+
}
|
|
453
|
+
catch (err) {
|
|
454
|
+
if (!running)
|
|
455
|
+
break;
|
|
456
|
+
warn(label, `publish-probe side poll failed: ${err?.message ?? err}`);
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
})();
|
|
460
|
+
const done = Promise.all([main, probes]);
|
|
359
461
|
return {
|
|
360
462
|
get running() {
|
|
361
463
|
return running;
|
|
362
464
|
},
|
|
465
|
+
get lastPassAt() {
|
|
466
|
+
return lastPassAt;
|
|
467
|
+
},
|
|
468
|
+
get lastAttemptAt() {
|
|
469
|
+
return lastAttemptAt;
|
|
470
|
+
},
|
|
363
471
|
async stop() {
|
|
364
472
|
running = false;
|
|
365
473
|
await done.catch(() => { });
|
|
@@ -5,12 +5,9 @@
|
|
|
5
5
|
*
|
|
6
6
|
* polls: [{ kind: 'sente-email', identityId: 'idt_...' }]
|
|
7
7
|
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
* with its own interval, backoff and backpressure, instead of every host
|
|
12
|
-
* hand-rolling a timer inside its message loop. Declaration travels with the
|
|
13
|
-
* agent; execution does not.
|
|
8
|
+
* The declaration and its PollRunner travel with the Agent. connectAsync starts
|
|
9
|
+
* both inbox delivery and external polls; disconnect stops both. The process
|
|
10
|
+
* runner does not own a second copy of the polling lifecycle.
|
|
14
11
|
*
|
|
15
12
|
* A poll answers ONE question: is there work for this agent right now. It never
|
|
16
13
|
* returns the work itself. The agent reads that for itself once woken, through
|
|
@@ -26,7 +23,7 @@
|
|
|
26
23
|
* Kind-specific config, verbatim from the agent's manifest.
|
|
27
24
|
*
|
|
28
25
|
* `chatId` is not kind-specific and is not optional in practice: a wake has to
|
|
29
|
-
* resolve to a lane or
|
|
26
|
+
* resolve to a lane or Agent's session resolver returns null and drops it
|
|
30
27
|
* silently. A poll has no engagement behind it to derive one from, so the
|
|
31
28
|
* binding names the chat the agent thinks in — which also puts its triage
|
|
32
29
|
* somewhere a human can watch. PollRunner refuses to register a binding without
|
|
@@ -5,12 +5,9 @@
|
|
|
5
5
|
*
|
|
6
6
|
* polls: [{ kind: 'sente-email', identityId: 'idt_...' }]
|
|
7
7
|
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
* with its own interval, backoff and backpressure, instead of every host
|
|
12
|
-
* hand-rolling a timer inside its message loop. Declaration travels with the
|
|
13
|
-
* agent; execution does not.
|
|
8
|
+
* The declaration and its PollRunner travel with the Agent. connectAsync starts
|
|
9
|
+
* both inbox delivery and external polls; disconnect stops both. The process
|
|
10
|
+
* runner does not own a second copy of the polling lifecycle.
|
|
14
11
|
*
|
|
15
12
|
* A poll answers ONE question: is there work for this agent right now. It never
|
|
16
13
|
* returns the work itself. The agent reads that for itself once woken, through
|
|
@@ -1,11 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* PollRunner — executes declared
|
|
2
|
+
* PollRunner — executes declared external wake sources for agents.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
* because none of it belongs to a host's message loop: a poll is per
|
|
8
|
-
* (agent, binding) and carries its own interval, backoff and backpressure.
|
|
4
|
+
* Each Agent owns its runner and starts it with its inbox loop. A poll is per
|
|
5
|
+
* (agent, binding), with its own interval, backoff and backpressure. This same
|
|
6
|
+
* lifecycle applies to fleet, MCP-tool, lab and evaluation agents.
|
|
9
7
|
*
|
|
10
8
|
* One loop per (agent, binding), and each loop AWAITS its own wake before
|
|
11
9
|
* scheduling the next tick. That is the whole backpressure story: while an
|
|
@@ -73,6 +71,7 @@ export declare class PollRunner {
|
|
|
73
71
|
private readonly wakeWindowMs;
|
|
74
72
|
private readonly loops;
|
|
75
73
|
private running;
|
|
74
|
+
private generation;
|
|
76
75
|
constructor({ pool, registry, agents, operatorKey, defaultIntervalMs, maxBackoffMs, wakeTimeoutMs, maxWakesPerWindow, wakeWindowMs, label, }: PollRunnerOptions);
|
|
77
76
|
get size(): number;
|
|
78
77
|
start(): void;
|
|
@@ -1,11 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* PollRunner — executes declared
|
|
2
|
+
* PollRunner — executes declared external wake sources for agents.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
* because none of it belongs to a host's message loop: a poll is per
|
|
8
|
-
* (agent, binding) and carries its own interval, backoff and backpressure.
|
|
4
|
+
* Each Agent owns its runner and starts it with its inbox loop. A poll is per
|
|
5
|
+
* (agent, binding), with its own interval, backoff and backpressure. This same
|
|
6
|
+
* lifecycle applies to fleet, MCP-tool, lab and evaluation agents.
|
|
9
7
|
*
|
|
10
8
|
* One loop per (agent, binding), and each loop AWAITS its own wake before
|
|
11
9
|
* scheduling the next tick. That is the whole backpressure story: while an
|
|
@@ -30,6 +28,7 @@ export class PollRunner {
|
|
|
30
28
|
wakeWindowMs;
|
|
31
29
|
loops = [];
|
|
32
30
|
running = false;
|
|
31
|
+
generation = 0;
|
|
33
32
|
constructor({ pool, registry, agents, operatorKey, defaultIntervalMs = 60_000, maxBackoffMs = 15 * 60_000, wakeTimeoutMs = Number(process.env.CLAUDE_SDK_TICK_TIMEOUT_MS ?? 300_000), maxWakesPerWindow = 20, wakeWindowMs = 60 * 60_000, label = 'PollRunner', }) {
|
|
34
33
|
this.pool = pool;
|
|
35
34
|
this.operatorKey = operatorKey;
|
|
@@ -65,6 +64,7 @@ export class PollRunner {
|
|
|
65
64
|
chatId: binding.chatId,
|
|
66
65
|
timer: null,
|
|
67
66
|
failures: 0,
|
|
67
|
+
inFlight: false,
|
|
68
68
|
wakes: 0,
|
|
69
69
|
windowStartedAt: Date.now(),
|
|
70
70
|
capped: false,
|
|
@@ -79,14 +79,18 @@ export class PollRunner {
|
|
|
79
79
|
if (this.running)
|
|
80
80
|
return;
|
|
81
81
|
this.running = true;
|
|
82
|
+
this.generation++;
|
|
82
83
|
if (!this.loops.length)
|
|
83
84
|
return;
|
|
84
|
-
for (const loop of this.loops)
|
|
85
|
-
|
|
85
|
+
for (const loop of this.loops) {
|
|
86
|
+
if (!loop.inFlight)
|
|
87
|
+
this.schedule(loop, 0);
|
|
88
|
+
}
|
|
86
89
|
runtimeLog.info(this.label, `started — ${this.loops.length} poll(s): ${this.loops.map((l) => `${l.agentId}:${l.kind.id}`).join(', ')}`);
|
|
87
90
|
}
|
|
88
91
|
stop() {
|
|
89
92
|
this.running = false;
|
|
93
|
+
this.generation++;
|
|
90
94
|
for (const loop of this.loops) {
|
|
91
95
|
if (loop.timer)
|
|
92
96
|
clearTimeout(loop.timer);
|
|
@@ -97,7 +101,14 @@ export class PollRunner {
|
|
|
97
101
|
schedule(loop, delayMs = this.nextDelay(loop)) {
|
|
98
102
|
if (!this.running)
|
|
99
103
|
return;
|
|
100
|
-
loop.timer = setTimeout(() =>
|
|
104
|
+
loop.timer = setTimeout(() => {
|
|
105
|
+
loop.timer = null;
|
|
106
|
+
loop.inFlight = true;
|
|
107
|
+
void this.tick(loop, this.generation).finally(() => {
|
|
108
|
+
loop.inFlight = false;
|
|
109
|
+
this.schedule(loop);
|
|
110
|
+
});
|
|
111
|
+
}, delayMs);
|
|
101
112
|
}
|
|
102
113
|
/** Interval while healthy; doubling up to the ceiling while failing. */
|
|
103
114
|
nextDelay(loop) {
|
|
@@ -105,7 +116,7 @@ export class PollRunner {
|
|
|
105
116
|
return loop.intervalMs;
|
|
106
117
|
return Math.min(loop.intervalMs * 2 ** loop.failures, this.maxBackoffMs);
|
|
107
118
|
}
|
|
108
|
-
async tick(loop) {
|
|
119
|
+
async tick(loop, generation = this.generation) {
|
|
109
120
|
if (!this.running)
|
|
110
121
|
return;
|
|
111
122
|
let result;
|
|
@@ -119,9 +130,11 @@ export class PollRunner {
|
|
|
119
130
|
catch (err) {
|
|
120
131
|
loop.failures += 1;
|
|
121
132
|
runtimeLog.error(this.label, `${loop.agentId}:${loop.kind.id} source failed (${loop.failures}x, next in ${this.nextDelay(loop)}ms): ${err.message}`);
|
|
122
|
-
this.schedule(loop);
|
|
123
133
|
return;
|
|
124
134
|
}
|
|
135
|
+
// A source read may finish after disconnect or a new connection.
|
|
136
|
+
if (!this.running || generation !== this.generation)
|
|
137
|
+
return;
|
|
125
138
|
if (result?.actionable && this.claimWake(loop)) {
|
|
126
139
|
const hint = result.hint ?? `New work from ${loop.kind.id}. Check it.`;
|
|
127
140
|
try {
|
|
@@ -140,7 +153,6 @@ export class PollRunner {
|
|
|
140
153
|
runtimeLog.error(this.label, `${loop.agentId} wake failed (${loop.failures}x, next in ${this.nextDelay(loop)}ms): ${err.message}`);
|
|
141
154
|
}
|
|
142
155
|
}
|
|
143
|
-
this.schedule(loop);
|
|
144
156
|
}
|
|
145
157
|
/**
|
|
146
158
|
* Take one wake from this binding's budget, or refuse.
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { AgentOptions } from '../../Agent.js';
|
|
2
|
+
import { PollRunner, type WakeTarget } from './PollRunner.js';
|
|
3
|
+
/** Validate before starting any work, so a declared wake source cannot vanish. */
|
|
4
|
+
export declare function createDeclaredPollRunner(options: AgentOptions, target: WakeTarget): PollRunner | null;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { PollRegistry } from './PollKind.js';
|
|
2
|
+
import { PollRunner } from './PollRunner.js';
|
|
3
|
+
/** Validate before starting any work, so a declared wake source cannot vanish. */
|
|
4
|
+
export function createDeclaredPollRunner(options, target) {
|
|
5
|
+
const registry = new PollRegistry();
|
|
6
|
+
for (const kind of options.pollKinds ?? [])
|
|
7
|
+
registry.register(kind);
|
|
8
|
+
if (options.polls === undefined)
|
|
9
|
+
return null;
|
|
10
|
+
if (!Array.isArray(options.polls))
|
|
11
|
+
throw new Error('Agent: polls must be an array');
|
|
12
|
+
for (const binding of options.polls) {
|
|
13
|
+
if (!binding || !registry.get(binding.kind)) {
|
|
14
|
+
throw new Error(`Agent ${options.agentId}: unknown poll kind "${binding?.kind}"; declare its implementation in pollKinds`);
|
|
15
|
+
}
|
|
16
|
+
if (typeof binding.chatId !== 'string' || !binding.chatId.trim()) {
|
|
17
|
+
throw new Error(`Agent ${options.agentId}: poll "${binding.kind}" requires a chatId`);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
if (!options.polls.length)
|
|
21
|
+
return null;
|
|
22
|
+
return new PollRunner({
|
|
23
|
+
pool: target,
|
|
24
|
+
registry,
|
|
25
|
+
agents: [{ agentId: options.agentId, polls: options.polls }],
|
|
26
|
+
operatorKey: options.operatorKey,
|
|
27
|
+
label: `${options.name || options.agentId}-polls`,
|
|
28
|
+
});
|
|
29
|
+
}
|
|
@@ -2,7 +2,7 @@ import { Client } from '@modelcontextprotocol/sdk/client/index.js';
|
|
|
2
2
|
import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js';
|
|
3
3
|
import { ConnectionsClient } from '@ziggs-ai/api-client';
|
|
4
4
|
import { defineTool } from '../../../tools/defineTool.js';
|
|
5
|
-
import { resolveZiggsGatewayTarget } from '../../../
|
|
5
|
+
import { resolveZiggsGatewayTarget } from '../../../mcp/createMcpAgent.js';
|
|
6
6
|
/**
|
|
7
7
|
* Calling a tool on a connected MCP server, one shot.
|
|
8
8
|
*
|
|
@@ -15,7 +15,7 @@ import { resolveZiggsGatewayTarget } from '../../../adapters/McpAdapter.js';
|
|
|
15
15
|
*
|
|
16
16
|
* The gateway (`POST /connections/:id/mcp`) has always been the right door and is
|
|
17
17
|
* already in production; what was missing was a tool that walks through it. The
|
|
18
|
-
* transport, headers and MCP handshake are the same ones `
|
|
18
|
+
* transport, headers and MCP handshake are the same ones `createMcpAgent`'s `ziggs`
|
|
19
19
|
* transport uses — reused rather than re-derived, so a change to the gateway
|
|
20
20
|
* contract lands in one place.
|
|
21
21
|
*
|
|
@@ -66,7 +66,10 @@ async function withClient(ctx, connectionId, grantId, run) {
|
|
|
66
66
|
grantId,
|
|
67
67
|
operatorKey: ctx['operatorKey'],
|
|
68
68
|
agentId: ctx['agentId'],
|
|
69
|
-
}, {
|
|
69
|
+
}, {
|
|
70
|
+
agentId: ctx['agentId'],
|
|
71
|
+
laneId: ctx['laneId'],
|
|
72
|
+
});
|
|
70
73
|
const transport = new StreamableHTTPClientTransport(target.url, {
|
|
71
74
|
requestInit: { headers: target.headers },
|
|
72
75
|
});
|
|
@@ -3,7 +3,7 @@ import type { Ctx, Identity } from '../types.js';
|
|
|
3
3
|
* The one place a per-lane `Ctx` is constructed.
|
|
4
4
|
*
|
|
5
5
|
* Both cognitions bootstrap the same Ctx: the FSM (`AgentMachine` constructor)
|
|
6
|
-
* and the Claude Agent SDK brain (`
|
|
6
|
+
* and the Claude Agent SDK brain (`ClaudeBrain.tick`). Before this, each
|
|
7
7
|
* carried its own copy of the fresh-Ctx literal and the restore-path defaulting,
|
|
8
8
|
* and they had already drifted (the "default them ONCE here" comment in
|
|
9
9
|
* AgentMachine no longer held). Keep it here so the shape lives in a single spot.
|
|
@@ -3,7 +3,7 @@ import { migrateLegacyTaskFields } from './taskBookkeeping.js';
|
|
|
3
3
|
* The one place a per-lane `Ctx` is constructed.
|
|
4
4
|
*
|
|
5
5
|
* Both cognitions bootstrap the same Ctx: the FSM (`AgentMachine` constructor)
|
|
6
|
-
* and the Claude Agent SDK brain (`
|
|
6
|
+
* and the Claude Agent SDK brain (`ClaudeBrain.tick`). Before this, each
|
|
7
7
|
* carried its own copy of the fresh-Ctx literal and the restore-path defaulting,
|
|
8
8
|
* and they had already drifted (the "default them ONCE here" comment in
|
|
9
9
|
* AgentMachine no longer held). Keep it here so the shape lives in a single spot.
|
|
@@ -162,7 +162,7 @@ function singleEventToOutcome(ev, ownAgentId) {
|
|
|
162
162
|
return null;
|
|
163
163
|
}
|
|
164
164
|
if (ev.type === 'state-timeout') {
|
|
165
|
-
// Synthetic frame from
|
|
165
|
+
// Synthetic frame from Agent's StateTimers — host-constructed, fields always present.
|
|
166
166
|
return { kind: 'state-timeout', state: ev.state, waitedMs: ev.waitedMs };
|
|
167
167
|
}
|
|
168
168
|
// Only real inbound chat messages should drive the FSM. Backend bookkeeping
|
|
@@ -15,7 +15,7 @@ interface RunTurnInput {
|
|
|
15
15
|
toolManager: ToolManager;
|
|
16
16
|
promptBuilder: PromptBuilder;
|
|
17
17
|
/**
|
|
18
|
-
* Agent-private long-term memory, threaded from
|
|
18
|
+
* Agent-private long-term memory, threaded from Agent. Currently
|
|
19
19
|
* surfaced into `eff` as part of the tool-call effect payload so tools
|
|
20
20
|
* can read/write notes; the prompt builder doesn't consume it yet.
|
|
21
21
|
*/
|
package/dist/runtime/runTurn.js
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
|
+
import { isAgreementId } from '@ziggs-ai/api-client';
|
|
1
2
|
import { outcomeFromActionResult } from './buildOutcome.js';
|
|
2
3
|
import { runtimeLog } from '../shared/runtimeLog.js';
|
|
3
4
|
import { safeParseJSON } from '../utils/jsonExtractor.js';
|
|
4
5
|
import { withDeclaredMandate } from './mandateDeclaration.js';
|
|
6
|
+
import { resolveOnBehalfAgreementId, } from '../brain/onBehalfAgreement.js';
|
|
5
7
|
const MAX_TOOL_LOOP_ITERS = 20;
|
|
6
8
|
const MAX_MSG_CHARS = 3500;
|
|
7
9
|
/**
|
|
@@ -117,9 +119,7 @@ export async function runTurn(input) {
|
|
|
117
119
|
// only. a bare agreement id as sessionId is a mis-keyed lane —
|
|
118
120
|
// loud-log the empty snapshot rather than staying silent.
|
|
119
121
|
if (ctxObj.chatMissing) {
|
|
120
|
-
const looksLikeBareAgreement =
|
|
121
|
-
sessionId.startsWith('agr_') &&
|
|
122
|
-
!sessionId.startsWith('agrn-');
|
|
122
|
+
const looksLikeBareAgreement = isAgreementId(sessionId);
|
|
123
123
|
if (looksLikeBareAgreement) {
|
|
124
124
|
runtimeLog.warn('runTurn', `chatMissing on non-chat sessionId=${sessionId} (state=${stateId}) — ` +
|
|
125
125
|
`lane was keyed by agreementId instead of the agreement's origin/notification chat; ` +
|
|
@@ -265,7 +265,9 @@ export async function runTurn(input) {
|
|
|
265
265
|
// not. The runtime knows; the model would have to be told.
|
|
266
266
|
args = withDeclaredMandate(args, toolName, toolManager, ctx.activeAgreementId);
|
|
267
267
|
const decision = { action: matchingAction, tool: toolName, args };
|
|
268
|
-
const result = await executeDecision(decision, actionDef, effWithTrace, sessionId, memory,
|
|
268
|
+
const result = await executeDecision(decision, actionDef, effWithTrace, sessionId, memory, incomingEvent, addToolUsage, ctx.identity.agentId, Array.isArray(ctxObj.agreements)
|
|
269
|
+
? ctxObj.agreements
|
|
270
|
+
: null);
|
|
269
271
|
toolCallResults.push({ tc, matchingAction, result, args, actionDef });
|
|
270
272
|
}
|
|
271
273
|
let hasTerminal = false;
|
|
@@ -418,7 +420,9 @@ export async function runTurn(input) {
|
|
|
418
420
|
try {
|
|
419
421
|
// effWithTrace, not eff: JSON-path tool calls must carry runId/stateId
|
|
420
422
|
// and the wake's write keys exactly like tool-loop calls do.
|
|
421
|
-
result = await executeDecision(decision, actionDef, effWithTrace, sessionId, memory,
|
|
423
|
+
result = await executeDecision(decision, actionDef, effWithTrace, sessionId, memory, incomingEvent, addToolUsage, ctx.identity.agentId, Array.isArray(ctxObj.agreements)
|
|
424
|
+
? ctxObj.agreements
|
|
425
|
+
: null);
|
|
422
426
|
}
|
|
423
427
|
catch (err) {
|
|
424
428
|
// Only outbound sends can throw out of executeDecision — executeTool
|
|
@@ -642,17 +646,18 @@ function findActionForTool(toolName, allowedActionNames, actions) {
|
|
|
642
646
|
}
|
|
643
647
|
return null;
|
|
644
648
|
}
|
|
645
|
-
async function executeDecision(decision, actionDef, eff, sessionId, memory, incomingEvent = null, onUsage) {
|
|
649
|
+
async function executeDecision(decision, actionDef, eff, sessionId, memory, incomingEvent = null, onUsage, agentId = null, agreements = null) {
|
|
646
650
|
const boundTool = actionDef?.tool || null;
|
|
647
651
|
if (boundTool) {
|
|
648
652
|
decision.tool = boundTool;
|
|
649
|
-
return await executeTool(decision, eff, sessionId, memory, incomingEvent, onUsage);
|
|
653
|
+
return await executeTool(decision, eff, sessionId, memory, incomingEvent, onUsage, agentId, agreements);
|
|
650
654
|
}
|
|
651
655
|
if (decision.action === '_genericDiscoveryTool' && decision.tool) {
|
|
652
|
-
return await executeTool(decision, eff, sessionId, memory, incomingEvent, onUsage);
|
|
656
|
+
return await executeTool(decision, eff, sessionId, memory, incomingEvent, onUsage, agentId, agreements);
|
|
657
|
+
}
|
|
658
|
+
if (decision.tool) {
|
|
659
|
+
return await executeTool(decision, eff, sessionId, memory, incomingEvent, onUsage, agentId, agreements);
|
|
653
660
|
}
|
|
654
|
-
if (decision.tool)
|
|
655
|
-
return await executeTool(decision, eff, sessionId, memory, incomingEvent, onUsage);
|
|
656
661
|
if (decision.messages && Array.isArray(decision.messages) && decision.messages.length > 0) {
|
|
657
662
|
return await executeSendMessages(decision, eff, sessionId);
|
|
658
663
|
}
|
|
@@ -775,7 +780,7 @@ async function executeSendMessages(decision, eff, sessionId) {
|
|
|
775
780
|
* dependencies (taskService, agreementService, agents list, etc.), and
|
|
776
781
|
* wrapping the call in telemetry / operation-record entries if it wants.
|
|
777
782
|
*/
|
|
778
|
-
async function executeTool(decision, eff, sessionId, memory, incomingEvent = null, onUsage) {
|
|
783
|
+
async function executeTool(decision, eff, sessionId, memory, incomingEvent = null, onUsage, agentId = null, agreements = null) {
|
|
779
784
|
const { tool, args } = decision;
|
|
780
785
|
if (!tool)
|
|
781
786
|
return null;
|
|
@@ -788,13 +793,8 @@ async function executeTool(decision, eff, sessionId, memory, incomingEvent = nul
|
|
|
788
793
|
memory,
|
|
789
794
|
senderId: incomingEvent?.senderId != null ? String(incomingEvent.senderId) : undefined,
|
|
790
795
|
senderType: incomingEvent?.senderType != null ? String(incomingEvent.senderType) : undefined,
|
|
791
|
-
//
|
|
792
|
-
|
|
793
|
-
// resolve the originating end-user, not the delegating agent (senderId).
|
|
794
|
-
onBehalfOfAgreementId: (() => {
|
|
795
|
-
const aid = incomingEvent?.result?.agreementId;
|
|
796
|
-
return typeof aid === 'string' && aid ? aid : undefined;
|
|
797
|
-
})(),
|
|
796
|
+
// Never bind on-behalf to a sub the agent only pays for.
|
|
797
|
+
onBehalfOfAgreementId: resolveOnBehalfAgreementId(incomingEvent ?? undefined, agreements, agentId, sessionId),
|
|
798
798
|
});
|
|
799
799
|
if (r.usage)
|
|
800
800
|
onUsage?.(r.usage);
|
|
@@ -94,7 +94,7 @@ export function validateWorkflow(workflow) {
|
|
|
94
94
|
errors.push(`thinking state "${name}" requires at least one action`);
|
|
95
95
|
}
|
|
96
96
|
if (state.timeoutMs !== undefined) {
|
|
97
|
-
errors.push(`state "${name}" is thinking but declares timeoutMs — liveness timers apply to parked states only (thinking turns are bounded by
|
|
97
|
+
errors.push(`state "${name}" is thinking but declares timeoutMs — liveness timers apply to parked states only (thinking turns are bounded by Agent tickTimeoutMs)`);
|
|
98
98
|
}
|
|
99
99
|
}
|
|
100
100
|
if (state.kind === 'mechanical') {
|
|
@@ -61,9 +61,14 @@ export declare function skillHash(files: SkillFile[]): string;
|
|
|
61
61
|
export declare function parseSkill(files: SkillFile[], opts?: {
|
|
62
62
|
maxChars?: number;
|
|
63
63
|
}): ParsedSkill;
|
|
64
|
+
/** Catalog name for a reference path (`references/tone.md` → `tone`). */
|
|
65
|
+
export declare function skillReferenceName(path: string): string;
|
|
66
|
+
/** First prose line of a reference, for the on-prompt catalog. */
|
|
67
|
+
export declare function skillReferenceOneLine(content: string): string;
|
|
64
68
|
/**
|
|
65
|
-
* Assemble the skill's role prompt: body
|
|
66
|
-
*
|
|
69
|
+
* Assemble the skill's role prompt: the body, then a catalog of references.
|
|
70
|
+
* Full reference text is fetched with `skill_read_reference` — it does not
|
|
71
|
+
* ride every turn.
|
|
67
72
|
*
|
|
68
73
|
* This is only the ROLE block. The Ziggs protocol is prepended separately, by
|
|
69
74
|
* the one place that builds a Claude-brain system prompt — a skill is never in a
|