@ziggs-ai/agent-sdk 0.10.0 → 0.10.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +23 -0
- package/dist/AgentHost.d.ts +50 -14
- package/dist/AgentHost.js +129 -7
- package/dist/agent/ClaudeSdkAgent.d.ts +23 -1
- package/dist/agent/ClaudeSdkAgent.js +145 -75
- package/dist/context/applyEffects.js +4 -3
- package/dist/index.d.ts +3 -2
- package/dist/index.js +3 -2
- package/dist/runtime/bootstrapCtx.js +6 -8
- package/dist/runtime/buildOutcome.d.ts +9 -1
- package/dist/runtime/buildOutcome.js +29 -6
- package/dist/runtime/taskIntake.js +6 -4
- package/dist/runtime/wakeWriteKeys.js +14 -21
- package/dist/server/ConnectionPool.d.ts +10 -52
- package/dist/server/ConnectionPool.js +26 -57
- package/dist/server/EventQueue.d.ts +6 -0
- package/dist/server/EventQueue.js +21 -6
- package/dist/server/SeenMessages.d.ts +7 -0
- package/dist/server/SeenMessages.js +15 -0
- package/dist/server/ZiggsEffectHandler.d.ts +1 -0
- package/dist/server/ZiggsEffectHandler.js +14 -7
- package/dist/server/inboxLoop.d.ts +17 -2
- package/dist/server/inboxLoop.js +92 -16
- package/dist/server/polls/PollKind.d.ts +72 -0
- package/dist/server/polls/PollKind.js +53 -0
- package/dist/server/polls/PollRunner.d.ts +62 -0
- package/dist/server/polls/PollRunner.js +138 -0
- package/dist/server/questMatch.d.ts +5 -0
- package/dist/server/questMatch.js +14 -0
- package/dist/server/runLauncher.d.ts +0 -6
- package/dist/server/runLauncher.js +54 -27
- package/dist/server/tools/tier1/protocolRunner.d.ts +3 -1
- package/dist/server/tools/tier1/protocolRunner.js +24 -28
- package/dist/server/tools/tier1/protocolTools.js +23 -13
- package/dist/server/tools/tier2/connectionTools.js +12 -1
- package/dist/tasks/protocolRegistry.d.ts +27 -0
- package/dist/tasks/protocolRegistry.js +36 -0
- package/dist/types.d.ts +0 -9
- package/package.json +2 -2
- package/dist/server/staleBacklogGate.d.ts +0 -57
- package/dist/server/staleBacklogGate.js +0 -100
|
@@ -1,9 +1,39 @@
|
|
|
1
1
|
import { ConnectionPool } from './ConnectionPool.js';
|
|
2
2
|
import { AgentHost } from '../AgentHost.js';
|
|
3
3
|
import { createHealthServer } from './createHealthServer.js';
|
|
4
|
-
import { createStaleBacklogGate, resolveReplayPolicy } from './staleBacklogGate.js';
|
|
5
4
|
import { boundKeyFleetWarning } from '../shared/operatorKeyClaims.js';
|
|
6
5
|
import { runtimeLog } from '../shared/runtimeLog.js';
|
|
6
|
+
import { PollRegistry } from './polls/PollKind.js';
|
|
7
|
+
import { PollRunner } from './polls/PollRunner.js';
|
|
8
|
+
/**
|
|
9
|
+
* Collect the poll kinds the agents themselves ship and start a runner for
|
|
10
|
+
* whatever they declared. This lives in the launcher, not in a caller's
|
|
11
|
+
* bootstrap file: an agent that declares `polls` and gets no runner sees no
|
|
12
|
+
* error and simply never wakes, which is indistinguishable from a quiet source.
|
|
13
|
+
* Every launcher path therefore executes polls, or refuses loudly.
|
|
14
|
+
*/
|
|
15
|
+
function startDeclaredPolls(configs, pool, { operatorKey, label }) {
|
|
16
|
+
const registry = new PollRegistry();
|
|
17
|
+
for (const cfg of configs) {
|
|
18
|
+
for (const kind of (cfg.pollKinds ?? [])) {
|
|
19
|
+
// Two agents may legitimately share a source; first registration wins and
|
|
20
|
+
// a duplicate id is not an error worth killing a fleet launch over.
|
|
21
|
+
if (!registry.get(kind.id))
|
|
22
|
+
registry.register(kind);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
const runner = new PollRunner({
|
|
26
|
+
pool,
|
|
27
|
+
registry,
|
|
28
|
+
agents: configs,
|
|
29
|
+
label: `${label}-polls`,
|
|
30
|
+
...(operatorKey ? { operatorKey } : {}),
|
|
31
|
+
});
|
|
32
|
+
if (runner.size === 0)
|
|
33
|
+
return null;
|
|
34
|
+
runner.start();
|
|
35
|
+
return runner;
|
|
36
|
+
}
|
|
7
37
|
export async function runLauncher(configOrConfigs, opts = {}) {
|
|
8
38
|
if (Array.isArray(configOrConfigs))
|
|
9
39
|
return runFleet(configOrConfigs, opts);
|
|
@@ -21,8 +51,11 @@ async function runSingle(config, { operatorKey, label = 'agent', port = process.
|
|
|
21
51
|
runtimeLog.warn(label, `Agent failed to connect at startup: ${err.message} — will retry`);
|
|
22
52
|
}
|
|
23
53
|
const server = healthServer ? createHealthServer({ port: Number(port), label }) : null;
|
|
54
|
+
// One agent, so the "pool" is just this host.
|
|
55
|
+
const pollRunner = startDeclaredPolls([decorated], { sendTo: (_agentId, text, metadata) => agent.handleMessage(text, metadata ?? {}) }, { label, ...(effectiveKey ? { operatorKey: effectiveKey } : {}) });
|
|
24
56
|
process.on('SIGTERM', async () => {
|
|
25
57
|
runtimeLog.info(label, 'SIGTERM — shutting down');
|
|
58
|
+
pollRunner?.stop();
|
|
26
59
|
agent.disconnect();
|
|
27
60
|
await agent.drain(30_000);
|
|
28
61
|
try {
|
|
@@ -36,14 +69,8 @@ async function runSingle(config, { operatorKey, label = 'agent', port = process.
|
|
|
36
69
|
});
|
|
37
70
|
return { agent, healthServer: server ?? undefined };
|
|
38
71
|
}
|
|
39
|
-
async function runFleet(configs, { operatorKey,
|
|
40
|
-
|
|
41
|
-
// (FLEET_REPLAY_MAX_AGE / FLEET_REPLAY_STALE); an explicit
|
|
42
|
-
// poolOptions.shouldLazyStart wins.
|
|
43
|
-
const pool = new ConnectionPool({
|
|
44
|
-
shouldLazyStart: createStaleBacklogGate({ ...resolveReplayPolicy(), label }),
|
|
45
|
-
...poolOptions,
|
|
46
|
-
});
|
|
72
|
+
async function runFleet(configs, { operatorKey, port = process.env.PORT || 8080, healthServer = true, label = 'launcher', agentMeta, primary: primaryConfig, onShutdown, } = {}) {
|
|
73
|
+
const pool = new ConnectionPool();
|
|
47
74
|
const effectiveKey = operatorKey ?? process.env.ZIGGS_OPERATOR_KEY;
|
|
48
75
|
const decoratedConfigs = effectiveKey
|
|
49
76
|
? configs.map(c => (c.operatorKey ? c : { ...c, operatorKey: effectiveKey }))
|
|
@@ -63,10 +90,6 @@ async function runFleet(configs, { operatorKey, wsUrl = process.env.WS_URL, port
|
|
|
63
90
|
const boundWarning = boundKeyFleetWarning(controlKey, pool.size);
|
|
64
91
|
if (boundWarning)
|
|
65
92
|
runtimeLog.warn(label, boundWarning);
|
|
66
|
-
pool.startControl({ wsUrl, operatorKey: controlKey });
|
|
67
|
-
}
|
|
68
|
-
else {
|
|
69
|
-
runtimeLog.warn(label, 'No operatorKey provided — skipping fleet inbox poll (no agents will be lazily started)');
|
|
70
93
|
}
|
|
71
94
|
let primary = null;
|
|
72
95
|
if (resolvedPrimaryConfig) {
|
|
@@ -76,26 +99,30 @@ async function runFleet(configs, { operatorKey, wsUrl = process.env.WS_URL, port
|
|
|
76
99
|
runtimeLog.info(label, `Primary agent "${resolvedPrimaryConfig.name || resolvedPrimaryConfig.ownAgentId || 'primary'}" connected`);
|
|
77
100
|
}
|
|
78
101
|
catch (err) {
|
|
79
|
-
// Backend may not be ready yet —
|
|
80
|
-
// Don't crash the process
|
|
102
|
+
// Backend may not be ready yet — the inbox long-poll loop retries on
|
|
103
|
+
// the next pass. Don't crash the process.
|
|
81
104
|
runtimeLog.warn(label, `Primary agent "${resolvedPrimaryConfig.name || resolvedPrimaryConfig.ownAgentId || 'primary'}" failed to connect at startup: ${err.message} — will retry`);
|
|
82
105
|
}
|
|
83
106
|
}
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
107
|
+
// Every registered agent connects (ZIG-1108). There is no roster to consult
|
|
108
|
+
// and no reason to defer: an agent this launcher was handed is an agent it
|
|
109
|
+
// hosts, and each host's own inbox long-poll is the only wake signal needed.
|
|
110
|
+
await pool.connectAll();
|
|
111
|
+
const connected = pool.listActive().length;
|
|
112
|
+
if (connected < pool.size) {
|
|
113
|
+
// Say it loudly and with the number. A launcher that silently hosts fewer
|
|
114
|
+
// agents than it was given is exactly how three published, hireable agents
|
|
115
|
+
// ran with no host and nothing noticed (ZIG-1107).
|
|
116
|
+
runtimeLog.warn(label, `Only ${connected}/${pool.size} agent(s) connected — the rest are registered but unhosted`);
|
|
93
117
|
}
|
|
94
|
-
runtimeLog.info(label, `Ready — ${pool.size} agent(s)
|
|
95
|
-
|
|
118
|
+
runtimeLog.info(label, `Ready — ${connected}/${pool.size} agent(s) connected`);
|
|
119
|
+
const pollRunner = startDeclaredPolls(decoratedConfigs, pool, {
|
|
120
|
+
label,
|
|
121
|
+
...(effectiveKey ? { operatorKey: effectiveKey } : {}),
|
|
122
|
+
});
|
|
96
123
|
process.on('SIGTERM', async () => {
|
|
97
124
|
runtimeLog.info(label, 'SIGTERM — shutting down');
|
|
98
|
-
|
|
125
|
+
pollRunner?.stop();
|
|
99
126
|
primary?.disconnect?.();
|
|
100
127
|
await primary?.drain?.(30_000);
|
|
101
128
|
await pool.disconnectAll();
|
|
@@ -36,5 +36,7 @@ interface ProtocolPayload {
|
|
|
36
36
|
})[];
|
|
37
37
|
[key: string]: unknown;
|
|
38
38
|
}
|
|
39
|
-
export declare function dispatchProtocolOp(services: Services, payload: ProtocolPayload, chatId: string | null, agents?: unknown[], actingAgentId?: string | null, wakeKeys?: WakeWriteKeys | null
|
|
39
|
+
export declare function dispatchProtocolOp(services: Services, payload: ProtocolPayload, chatId: string | null, agents?: unknown[], actingAgentId?: string | null, wakeKeys?: WakeWriteKeys | null,
|
|
40
|
+
/** ZIG-1120 #8 — prefer the cached session snapshot over a fresh HTTP read. */
|
|
41
|
+
cachedAgreements?: unknown[]): Promise<unknown>;
|
|
40
42
|
export {};
|
|
@@ -1,17 +1,9 @@
|
|
|
1
|
-
import { isBroadcastTarget,
|
|
1
|
+
import { isBroadcastTarget, } from '@ziggs-ai/api-client';
|
|
2
2
|
import { applyFleetCounterPricing, applyFleetPricingForProtocol, } from '../../../pricing/fleetDefaults.js';
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
function shapeAgreementResult(agreement) {
|
|
8
|
-
if (!agreement)
|
|
9
|
-
return agreement;
|
|
10
|
-
return agreement.engagementKind === 'link' ? linkSummary(agreement) : agreement;
|
|
11
|
-
}
|
|
12
|
-
function shapeRevokeResult(result) {
|
|
13
|
-
return { ...result, agreement: shapeAgreementResult(result.agreement) };
|
|
14
|
-
}
|
|
3
|
+
// ZIG-1111: the link-summary rule lives in AgreementClient, at the point the
|
|
4
|
+
// document is parsed. Every verb here inherits it through the client instead of
|
|
5
|
+
// each one restating `engagementKind === 'link' ? linkSummary(a) : a` — which is
|
|
6
|
+
// how counter, fulfill and subcontract came to return the raw document.
|
|
15
7
|
// Map legacy task-state aliases onto the canonical set.
|
|
16
8
|
function normalizeState(state) {
|
|
17
9
|
if (state === 'done')
|
|
@@ -53,7 +45,9 @@ function normalizePlanInPlace(payload) {
|
|
|
53
45
|
? 'before_execution'
|
|
54
46
|
: 'with_proposal';
|
|
55
47
|
}
|
|
56
|
-
export async function dispatchProtocolOp(services, payload, chatId, agents = [], actingAgentId = null, wakeKeys = null
|
|
48
|
+
export async function dispatchProtocolOp(services, payload, chatId, agents = [], actingAgentId = null, wakeKeys = null,
|
|
49
|
+
/** ZIG-1120 #8 — prefer the cached session snapshot over a fresh HTTP read. */
|
|
50
|
+
cachedAgreements) {
|
|
57
51
|
if (!payload?.operation)
|
|
58
52
|
return null;
|
|
59
53
|
const { operation } = payload;
|
|
@@ -63,20 +57,16 @@ export async function dispatchProtocolOp(services, payload, chatId, agents = [],
|
|
|
63
57
|
case 'agreement-propose':
|
|
64
58
|
return proposeAgreement(services, payload, chatId, agents, actorId, wakeKeys);
|
|
65
59
|
case 'agreement-subcontract':
|
|
66
|
-
return subcontractAgreement(services, payload, chatId, agents, actorId, wakeKeys);
|
|
60
|
+
return subcontractAgreement(services, payload, chatId, agents, actorId, wakeKeys, cachedAgreements);
|
|
67
61
|
case 'agreement-respond':
|
|
68
62
|
if (!payload.agreementId || !payload.action)
|
|
69
63
|
return null;
|
|
70
|
-
return
|
|
64
|
+
return agreementService.respond(payload.agreementId, payload.action);
|
|
71
65
|
case 'agreement-claim': {
|
|
72
66
|
// ZIG-1021: the one claim verb — quest, standing offer, or link invite.
|
|
73
67
|
if (!payload.agreementId)
|
|
74
68
|
throw new Error('agreement-claim requires `agreementId`');
|
|
75
|
-
|
|
76
|
-
return {
|
|
77
|
-
...claimed,
|
|
78
|
-
agreement: shapeAgreementResult(claimed.agreement),
|
|
79
|
-
};
|
|
69
|
+
return await agreementService.claim(payload.agreementId);
|
|
80
70
|
}
|
|
81
71
|
case 'agreement-counter-proposal':
|
|
82
72
|
if (!payload.agreementId)
|
|
@@ -101,16 +91,16 @@ export async function dispatchProtocolOp(services, payload, chatId, agents = [],
|
|
|
101
91
|
case 'agreement-revoke':
|
|
102
92
|
if (!payload.agreementId)
|
|
103
93
|
return null;
|
|
104
|
-
return
|
|
94
|
+
return await agreementService.revoke(payload.agreementId);
|
|
105
95
|
case 'agreement-fulfill':
|
|
106
96
|
if (!payload.agreementId)
|
|
107
97
|
return null;
|
|
108
|
-
return
|
|
98
|
+
return await agreementService.fulfill(payload.agreementId);
|
|
109
99
|
case 'task-spawn': {
|
|
110
100
|
if (!payload.agreementId || !payload.description)
|
|
111
101
|
return null;
|
|
112
102
|
let spawnAgreementId = payload.agreementId;
|
|
113
|
-
const ags = await agreementsInChat(services, chatId);
|
|
103
|
+
const ags = await agreementsInChat(services, chatId, cachedAgreements);
|
|
114
104
|
// b2 (ZIG-832): if the assignee is not a party on the cited agreement,
|
|
115
105
|
// redirect the spawn to the ACTIVE sub-agreement (same root) where the
|
|
116
106
|
// assignee IS the provider — the orchestrator often cites the top-level
|
|
@@ -229,7 +219,7 @@ async function proposeAgreement(services, payload, chatId, _agents, actingAgentI
|
|
|
229
219
|
if (isBroadcast) {
|
|
230
220
|
throw new Error('A broadcast link is an invite — use link_create_invite instead; propose links to one agent id.');
|
|
231
221
|
}
|
|
232
|
-
return
|
|
222
|
+
return services.agreementService.requestLink(payload.proposedTo, payload.description);
|
|
233
223
|
}
|
|
234
224
|
if (!isBroadcast && !payload.providerId) {
|
|
235
225
|
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).');
|
|
@@ -274,9 +264,15 @@ async function proposeAgreement(services, payload, chatId, _agents, actingAgentI
|
|
|
274
264
|
// rejects with 400 "assignee is not a party". Both are made deterministic here
|
|
275
265
|
// using the chat's existing agreements.
|
|
276
266
|
const TERMINAL_STATUS = new Set(['fulfilled', 'cancelled', 'refunded', 'rejected']);
|
|
277
|
-
async function agreementsInChat(services, chatId) {
|
|
267
|
+
async function agreementsInChat(services, chatId, cachedAgreements) {
|
|
278
268
|
if (!chatId)
|
|
279
269
|
return [];
|
|
270
|
+
// ZIG-1120 #8 — createZiggsEffectHandler already cached the session snapshot;
|
|
271
|
+
// prefer that over another GET /context/snapshot just for the agreements array.
|
|
272
|
+
// Empty array is a real answer ("none in this chat") — do not re-fetch.
|
|
273
|
+
if (cachedAgreements !== undefined) {
|
|
274
|
+
return Array.isArray(cachedAgreements) ? cachedAgreements : [];
|
|
275
|
+
}
|
|
280
276
|
try {
|
|
281
277
|
const snap = await services.contextReadClient.snapshot(chatId, { maxMessages: 1 });
|
|
282
278
|
const ags = snap.agreements;
|
|
@@ -305,7 +301,7 @@ function agActive(a) {
|
|
|
305
301
|
function agIsActiveStatus(a) {
|
|
306
302
|
return String(a.status ?? '') === 'active';
|
|
307
303
|
}
|
|
308
|
-
async function subcontractAgreement(services, payload, chatId, _agents, actingAgentId, wakeKeys = null) {
|
|
304
|
+
async function subcontractAgreement(services, payload, chatId, _agents, actingAgentId, wakeKeys = null, cachedAgreements) {
|
|
309
305
|
if (!payload.description || !payload.executorId)
|
|
310
306
|
return null;
|
|
311
307
|
// b1 (ZIG-832): idempotent subcontract — if a non-terminal sub-agreement for
|
|
@@ -313,7 +309,7 @@ async function subcontractAgreement(services, payload, chatId, _agents, actingAg
|
|
|
313
309
|
// creating a duplicate (which bloats the chain and blocks the fulfill cascade).
|
|
314
310
|
if (chatId) {
|
|
315
311
|
const parent = payload.parentAgreementId;
|
|
316
|
-
const existing = (await agreementsInChat(services, chatId)).find((a) => agActive(a) && agIsProvider(a, payload.executorId) &&
|
|
312
|
+
const existing = (await agreementsInChat(services, chatId, cachedAgreements)).find((a) => agActive(a) && agIsProvider(a, payload.executorId) &&
|
|
317
313
|
(!parent || a.parentAgreementId === parent || a.rootAgreementId === parent));
|
|
318
314
|
if (existing?.agreementId)
|
|
319
315
|
return existing;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { defineTool } from '../../../tools/defineTool.js';
|
|
2
|
+
import { requireProtocolOperation } from '../../../tasks/protocolRegistry.js';
|
|
2
3
|
import { dispatchProtocolOp } from './protocolRunner.js';
|
|
3
4
|
/** ZIG-957 — keep these enums in lockstep with MCP ziggs_agreement_* / ziggs_task_create. */
|
|
4
5
|
const LIFECYCLE_ENUM = ['open', 'time-bound', 'count-bound'];
|
|
@@ -13,7 +14,16 @@ const planReviewTimingParam = {
|
|
|
13
14
|
enum: [...PLAN_REVIEW_TIMING_ENUM],
|
|
14
15
|
description: 'When the buyer reviews the plan: with_proposal | before_execution',
|
|
15
16
|
};
|
|
16
|
-
|
|
17
|
+
/**
|
|
18
|
+
* The handler for a protocol verb, looked up by tool name in the registry
|
|
19
|
+
* (ZIG-1110). Keying off the tool name rather than restating the operation is
|
|
20
|
+
* what makes half-registration impossible: a verb missing from
|
|
21
|
+
* PROTOCOL_TOOL_TO_OPERATION throws here, while this module loads, instead of
|
|
22
|
+
* dispatching correctly and then being unrecognised as protocol at every
|
|
23
|
+
* consumer that decides whether its result reaches context.
|
|
24
|
+
*/
|
|
25
|
+
function handlerFor(toolName) {
|
|
26
|
+
const operation = requireProtocolOperation(toolName);
|
|
17
27
|
return async (args, ctx) => {
|
|
18
28
|
const c = ctx;
|
|
19
29
|
if (!c.taskService)
|
|
@@ -26,16 +36,16 @@ function handlerFor(operation) {
|
|
|
26
36
|
taskService: c.taskService,
|
|
27
37
|
agreementService: c.agreementService,
|
|
28
38
|
contextReadClient: c.contextReadClient,
|
|
29
|
-
}, { operation, ...args }, c.chatId ?? null, c.agents ?? [], typeof c.agentId === 'string' ? c.agentId : null, c.wakeKeys ?? null);
|
|
39
|
+
}, { operation, ...args }, c.chatId ?? null, c.agents ?? [], typeof c.agentId === 'string' ? c.agentId : null, c.wakeKeys ?? null, Array.isArray(c.agreements) ? c.agreements : undefined);
|
|
30
40
|
if (result == null)
|
|
31
41
|
throw new Error(`Operation ${operation} returned no result`);
|
|
32
42
|
return result;
|
|
33
43
|
};
|
|
34
44
|
}
|
|
35
|
-
export const agreementProposeTool = defineTool('agreement_propose', { description: { type: 'string', required: true }, proposedTo: { type: 'string', required: true }, engagementKind: { type: 'string', enum: ['hire', 'service', 'link'], description: "Contract type. Defaults to service. 'link' proposes a bilateral trust link (no work, no money)." }, parentAgreementId: 'string', providerId: { type: 'string', description: 'Who does the work — required on direct proposals. Pass your own id when YOU are offering to do the work, or the proposedTo id when you are commissioning the RECIPIENT (any other third party needs a matching published offer). On a broadcast: omit for a quest (the claimer works, you pay), or pass your own id to publish a standing offer (you work, the claimer pays). The payer is always derived as the non-providing side.' }, price: { type: 'number', description: 'Amount in CENTS — 500 means $5.00, shown as ϟ5.00 in the UI. Convert both ways: a human asking for "ϟ2" or "$2 per task" means 200, not 2. ϟ is the currency symbol, not a cent.' }, lifecycle: lifecycleParam, expiresAt: 'string', maxExecutions: 'integer', 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' }, handlerFor('
|
|
36
|
-
export const agreementSubcontractTool = defineTool('agreement_subcontract', { description: { type: 'string', required: true }, executorId: { type: 'string', required: true }, parentAgreementId: { type: 'string', required: true }, payerId: 'string', price: { type: 'number', description: 'Amount in CENTS — 500 means $5.00, shown as ϟ5.00 in the UI. Convert both ways: a human asking for "ϟ2" or "$2 per task" means 200, not 2. ϟ is the currency symbol, not a cent.' }, lifecycle: lifecycleParam, expiresAt: 'string', maxExecutions: 'integer', agreementDescription: 'string' }, handlerFor('
|
|
37
|
-
export const agreementRespondTool = defineTool('agreement_respond', { agreementId: { type: 'string', required: true }, action: { type: 'string', required: true, enum: ['approve', 'reject'] } }, handlerFor('
|
|
38
|
-
export const agreementClaimTool = defineTool('agreement_claim', { agreementId: { type: 'string', required: true, description: 'The open agreement to claim (quest / offer / link invite id)' } }, handlerFor('
|
|
45
|
+
export const agreementProposeTool = defineTool('agreement_propose', { description: { type: 'string', required: true }, proposedTo: { type: 'string', required: true }, engagementKind: { type: 'string', enum: ['hire', 'service', 'link'], description: "Contract type. Defaults to service. 'link' proposes a bilateral trust link (no work, no money)." }, parentAgreementId: 'string', providerId: { type: 'string', description: 'Who does the work — required on direct proposals. Pass your own id when YOU are offering to do the work, or the proposedTo id when you are commissioning the RECIPIENT (any other third party needs a matching published offer). On a broadcast: omit for a quest (the claimer works, you pay), or pass your own id to publish a standing offer (you work, the claimer pays). The payer is always derived as the non-providing side.' }, price: { type: 'number', description: 'Amount in CENTS — 500 means $5.00, shown as ϟ5.00 in the UI. Convert both ways: a human asking for "ϟ2" or "$2 per task" means 200, not 2. ϟ is the currency symbol, not a cent.' }, lifecycle: lifecycleParam, expiresAt: 'string', maxExecutions: 'integer', 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' }, handlerFor('agreement_propose'), { description: 'Propose an agreement — direct, broadcast, or link; there are no separate publish tools. Direction is stated, never guessed: providerId = your own id ("I work, you pay") or providerId = proposedTo ("you work, I pay") on direct proposals. Broadcast with proposedTo="everyone" (fully public) or "org" (your org only): no providerId publishes a QUEST any agent may claim; providerId = your own id publishes a STANDING OFFER (claiming is agreement_claim; browsing is marketplace_view). engagementKind "link" with proposedTo = an agent id proposes bilateral trust. 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 });
|
|
46
|
+
export const agreementSubcontractTool = defineTool('agreement_subcontract', { description: { type: 'string', required: true }, executorId: { type: 'string', required: true }, parentAgreementId: { type: 'string', required: true }, payerId: 'string', price: { type: 'number', description: 'Amount in CENTS — 500 means $5.00, shown as ϟ5.00 in the UI. Convert both ways: a human asking for "ϟ2" or "$2 per task" means 200, not 2. ϟ is the currency symbol, not a cent.' }, lifecycle: lifecycleParam, expiresAt: 'string', maxExecutions: 'integer', agreementDescription: 'string' }, handlerFor('agreement_subcontract'), { description: 'Delegate work to another agent under an existing parent agreement. Requires parentAgreementId.', isAgreementCreation: true });
|
|
47
|
+
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 DIRECT agreement proposal addressed to you. Open broadcasts (quests, standing offers, link invites) have no personal approval slot — claim those with agreement_claim instead, or ignore them to pass. ONE exception: a hand-off that pins YOU as provider carries your pending approval even as an open broadcast — approving consents to serving whoever claims it (the row stays open for claims); rejecting cancels the hand-off. Ignoring it leaves the hand-off unclaimable, so always respond.' });
|
|
48
|
+
export const agreementClaimTool = defineTool('agreement_claim', { agreementId: { type: 'string', required: true, description: 'The open agreement to claim (quest / offer / link invite id)' } }, handlerFor('agreement_claim'), { description: 'Claim an open broadcast agreement by id — a quest (you do the work, the publisher pays), a standing offer (you buy, the publisher works), a hand-off (providerPinned: the publisher\'s hired agent works FOR you — you become the customer, and the payer when it is priced), or a link invite (bilateral trust forms immediately). You become the open party and the agreement activates. A hand-off is claimable only after its provider has accepted (409 until then). Find quests/offers with marketplace_view; direct proposals are approved with agreement_respond instead, not claimed.' });
|
|
39
49
|
export const taskSetResultTool = defineTool('task_set_result', {
|
|
40
50
|
taskId: { type: 'string', required: true },
|
|
41
51
|
// ZIG-957: same param name as MCP ziggs_task_set_result / PATCH /tasks/:id/state.
|
|
@@ -45,11 +55,11 @@ export const taskSetResultTool = defineTool('task_set_result', {
|
|
|
45
55
|
type: 'object',
|
|
46
56
|
description: 'Outcome payload (summary, status, links, …). Finished work is the task result — never a chat message.',
|
|
47
57
|
},
|
|
48
|
-
}, handlerFor('
|
|
49
|
-
export const agreementCounterTool = defineTool('agreement_counter', { agreementId: { type: 'string', required: true }, price: { type: 'number', description: 'Amount in CENTS — 500 means $5.00, shown as ϟ5.00 in the UI. Convert both ways: a human asking for "ϟ2" or "$2 per task" means 200, not 2. ϟ is the currency symbol, not a cent.' }, agreementDescription: 'string', expiresAt: 'string', lifecycle: lifecycleParam, maxExecutions: 'integer', description: 'string' }, handlerFor('
|
|
50
|
-
export const agreementGetTool = defineTool('agreement_get', { agreementId: { type: 'string', required: true } }, handlerFor('
|
|
51
|
-
export const agreementRevokeTool = defineTool('agreement_revoke', { agreementId: { type: 'string', required: true } }, handlerFor('
|
|
52
|
-
export const agreementFulfillTool = defineTool('agreement_fulfill', { agreementId: { type: 'string', required: true } }, handlerFor('
|
|
58
|
+
}, handlerFor('task_set_result'), { description: 'Transition a task to its terminal state — completed, failed, or cancelled — and record the result. Finished work is the task result: set it here as an object ({ summary, status, links }), never as a chat message. Pass state (not status).' });
|
|
59
|
+
export const agreementCounterTool = defineTool('agreement_counter', { agreementId: { type: 'string', required: true }, price: { type: 'number', description: 'Amount in CENTS — 500 means $5.00, shown as ϟ5.00 in the UI. Convert both ways: a human asking for "ϟ2" or "$2 per task" means 200, not 2. ϟ is the currency symbol, not a cent.' }, agreementDescription: 'string', expiresAt: 'string', lifecycle: lifecycleParam, maxExecutions: 'integer', description: 'string' }, handlerFor('agreement_counter'), { description: 'Counter a pending proposal with revised terms instead of approving or rejecting. Check status first with agreement_get.' });
|
|
60
|
+
export const agreementGetTool = defineTool('agreement_get', { agreementId: { type: 'string', required: true } }, handlerFor('agreement_get'), { description: 'Read the current proposal status for an agreement (pending, approved, rejected, countered, expired).' });
|
|
61
|
+
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.' });
|
|
62
|
+
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_set_result (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.' });
|
|
53
63
|
// `status` names its four values here because nothing else does: the backend is
|
|
54
64
|
// the only place the vocabulary was written down, and it now rejects anything
|
|
55
65
|
// outside it. Every reader (the chat step badge, the SDK's own plan formatter)
|
|
@@ -64,8 +74,8 @@ export const taskUpdatePlanStepTool = defineTool('task_update_plan_step', {
|
|
|
64
74
|
enum: ['pending', 'in_progress', 'completed', 'skipped'],
|
|
65
75
|
},
|
|
66
76
|
result: 'string',
|
|
67
|
-
}, handlerFor('
|
|
68
|
-
export const taskCreateTool = defineTool('task_create', { 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: '
|
|
77
|
+
}, handlerFor('task_update_plan_step'), { description: "Update the status of a single step within a task's plan." });
|
|
78
|
+
export const taskCreateTool = defineTool('task_create', { 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: 'Prior-step output artifact handles, for structured input links.' }, plan: { type: 'object' }, planReviewTiming: planReviewTimingParam, requireMidWorkPlanAck: 'boolean' }, handlerFor('task_create'), { description: 'Create 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.' });
|
|
69
79
|
export const PROTOCOL_TOOLS = [
|
|
70
80
|
agreementProposeTool, agreementSubcontractTool,
|
|
71
81
|
agreementRespondTool, agreementClaimTool, agreementCounterTool, agreementGetTool, agreementRevokeTool,
|
|
@@ -7,7 +7,13 @@ import { toolFromCapability } from '../../../tools/capabilityAdapter.js';
|
|
|
7
7
|
// allowance in the parity table).
|
|
8
8
|
export const connectionProxyTool = toolFromCapability(connectionProxyCapability);
|
|
9
9
|
export const requestConnectionTool = toolFromCapability(requestConnectionCapability);
|
|
10
|
-
export const connectionListGrantsTool = defineTool('connection_list_grants', {
|
|
10
|
+
export const connectionListGrantsTool = defineTool('connection_list_grants', {
|
|
11
|
+
connectionId: {
|
|
12
|
+
type: 'string',
|
|
13
|
+
required: true,
|
|
14
|
+
description: 'The stored connection to list grants for.',
|
|
15
|
+
},
|
|
16
|
+
}, async (args, ctx) => {
|
|
11
17
|
if (!args['connectionId'])
|
|
12
18
|
throw new Error('connectionId is required');
|
|
13
19
|
const operatorKey = ctx?.['operatorKey'];
|
|
@@ -28,6 +34,11 @@ export const connectionListGrantsTool = defineTool('connection_list_grants', { c
|
|
|
28
34
|
wrapped.body = err.body;
|
|
29
35
|
throw wrapped;
|
|
30
36
|
}
|
|
37
|
+
}, {
|
|
38
|
+
// ZIG-1128: this shipped with no description at all — the model was handed a
|
|
39
|
+
// name and a parameter and left to guess what the tool was for. Found by the
|
|
40
|
+
// new SDK tool-text guard.
|
|
41
|
+
description: "List the grants issued to this agent on one stored connection. Returns the grantId that connection_proxy needs. Use it when you have a connectionId but not the grantId — grant_list (scopeKind=connection) is the wider read across every connection.",
|
|
31
42
|
});
|
|
32
43
|
export const CONNECTION_TOOLS = [
|
|
33
44
|
connectionProxyTool,
|
|
@@ -1,3 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The protocol verbs and the state operation each one performs.
|
|
3
|
+
*
|
|
4
|
+
* This is the single place the set is written down (ZIG-1110). It used to be
|
|
5
|
+
* restated: the tool definitions passed their operation string to `handlerFor`
|
|
6
|
+
* by hand, and this table listed them again. `agreement_fulfill` was in the
|
|
7
|
+
* first list and not the second, so it dispatched fine but
|
|
8
|
+
* `isProtocolToolName` said no — and at every consumer (applyEffects,
|
|
9
|
+
* routingLabels, buildOutcome) the verb that ends an engagement permanently
|
|
10
|
+
* degraded to an unlabelled plain tool-result whose outcome never reached
|
|
11
|
+
* context.
|
|
12
|
+
*
|
|
13
|
+
* A protocol tool now takes its handler from this table BY TOOL NAME, and a
|
|
14
|
+
* name that is absent throws while the module is loading. Adding a verb in one
|
|
15
|
+
* place can no longer half-register it: the tool cannot be constructed at all
|
|
16
|
+
* until it appears here.
|
|
17
|
+
*
|
|
18
|
+
* This module deliberately imports nothing. It is read from the context and
|
|
19
|
+
* runtime paths, which must not pull in the server tool chain.
|
|
20
|
+
*/
|
|
1
21
|
export declare const PROTOCOL_TOOL_TO_OPERATION: Readonly<{
|
|
2
22
|
readonly agreement_propose: "agreement-propose";
|
|
3
23
|
readonly agreement_subcontract: "agreement-subcontract";
|
|
@@ -6,6 +26,7 @@ export declare const PROTOCOL_TOOL_TO_OPERATION: Readonly<{
|
|
|
6
26
|
readonly agreement_counter: "agreement-counter-proposal";
|
|
7
27
|
readonly agreement_get: "agreement-check-proposal";
|
|
8
28
|
readonly agreement_revoke: "agreement-revoke";
|
|
29
|
+
readonly agreement_fulfill: "agreement-fulfill";
|
|
9
30
|
readonly task_create: "task-spawn";
|
|
10
31
|
readonly task_set_result: "task-update";
|
|
11
32
|
readonly task_update_plan_step: "task-update-plan-step";
|
|
@@ -15,3 +36,9 @@ export type ProtocolOperation = (typeof PROTOCOL_TOOL_TO_OPERATION)[ProtocolTool
|
|
|
15
36
|
export declare const PROTOCOL_TOOL_NAMES: readonly ProtocolToolName[];
|
|
16
37
|
export declare function mapProtocolToolToOperation(toolName: string): ProtocolOperation | null;
|
|
17
38
|
export declare function isProtocolToolName(toolName: string): toolName is ProtocolToolName;
|
|
39
|
+
/**
|
|
40
|
+
* The operation a protocol tool performs, or a throw. Called at module load by
|
|
41
|
+
* the tool definitions, so a verb defined without a registry entry takes the
|
|
42
|
+
* import down instead of shipping half-registered.
|
|
43
|
+
*/
|
|
44
|
+
export declare function requireProtocolOperation(toolName: string): ProtocolOperation;
|
|
@@ -1,3 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The protocol verbs and the state operation each one performs.
|
|
3
|
+
*
|
|
4
|
+
* This is the single place the set is written down (ZIG-1110). It used to be
|
|
5
|
+
* restated: the tool definitions passed their operation string to `handlerFor`
|
|
6
|
+
* by hand, and this table listed them again. `agreement_fulfill` was in the
|
|
7
|
+
* first list and not the second, so it dispatched fine but
|
|
8
|
+
* `isProtocolToolName` said no — and at every consumer (applyEffects,
|
|
9
|
+
* routingLabels, buildOutcome) the verb that ends an engagement permanently
|
|
10
|
+
* degraded to an unlabelled plain tool-result whose outcome never reached
|
|
11
|
+
* context.
|
|
12
|
+
*
|
|
13
|
+
* A protocol tool now takes its handler from this table BY TOOL NAME, and a
|
|
14
|
+
* name that is absent throws while the module is loading. Adding a verb in one
|
|
15
|
+
* place can no longer half-register it: the tool cannot be constructed at all
|
|
16
|
+
* until it appears here.
|
|
17
|
+
*
|
|
18
|
+
* This module deliberately imports nothing. It is read from the context and
|
|
19
|
+
* runtime paths, which must not pull in the server tool chain.
|
|
20
|
+
*/
|
|
1
21
|
export const PROTOCOL_TOOL_TO_OPERATION = Object.freeze({
|
|
2
22
|
agreement_propose: 'agreement-propose',
|
|
3
23
|
agreement_subcontract: 'agreement-subcontract',
|
|
@@ -6,6 +26,7 @@ export const PROTOCOL_TOOL_TO_OPERATION = Object.freeze({
|
|
|
6
26
|
agreement_counter: 'agreement-counter-proposal',
|
|
7
27
|
agreement_get: 'agreement-check-proposal',
|
|
8
28
|
agreement_revoke: 'agreement-revoke',
|
|
29
|
+
agreement_fulfill: 'agreement-fulfill',
|
|
9
30
|
task_create: 'task-spawn',
|
|
10
31
|
task_set_result: 'task-update',
|
|
11
32
|
task_update_plan_step: 'task-update-plan-step',
|
|
@@ -19,3 +40,18 @@ export function mapProtocolToolToOperation(toolName) {
|
|
|
19
40
|
export function isProtocolToolName(toolName) {
|
|
20
41
|
return mapProtocolToolToOperation(toolName) != null;
|
|
21
42
|
}
|
|
43
|
+
/**
|
|
44
|
+
* The operation a protocol tool performs, or a throw. Called at module load by
|
|
45
|
+
* the tool definitions, so a verb defined without a registry entry takes the
|
|
46
|
+
* import down instead of shipping half-registered.
|
|
47
|
+
*/
|
|
48
|
+
export function requireProtocolOperation(toolName) {
|
|
49
|
+
const operation = mapProtocolToolToOperation(toolName);
|
|
50
|
+
if (!operation) {
|
|
51
|
+
throw new Error(`Unknown protocol tool "${toolName}": add it to PROTOCOL_TOOL_TO_OPERATION ` +
|
|
52
|
+
'(agent-sdk/src/tasks/protocolRegistry.ts). Every consumer of the protocol ' +
|
|
53
|
+
'set reads that table — a tool missing from it dispatches but is never ' +
|
|
54
|
+
'recognised as protocol, so its result never reaches context.');
|
|
55
|
+
}
|
|
56
|
+
return operation;
|
|
57
|
+
}
|
package/dist/types.d.ts
CHANGED
|
@@ -187,15 +187,6 @@ export interface Ctx {
|
|
|
187
187
|
/** Focus agreement — sync'd with tasks[activeTaskId] when a task is focused. */
|
|
188
188
|
activeAgreementId: string | null;
|
|
189
189
|
pendingProposalAgreementId: string | null;
|
|
190
|
-
/**
|
|
191
|
-
* @deprecated ZIG-1057 — unused mirrors; always `{}` / `[]` after bootstrap.
|
|
192
|
-
* Read via `allPendingSubtasks` / `allCollectedResults` / `allDelegatedAgreementIds`.
|
|
193
|
-
*/
|
|
194
|
-
pendingSubtasks: Record<string, PendingSubtask>;
|
|
195
|
-
/** @deprecated ZIG-1057 — see `pendingSubtasks`. */
|
|
196
|
-
collectedResults: CollectedSubtaskResult[];
|
|
197
|
-
/** @deprecated ZIG-1057 — see `pendingSubtasks`. */
|
|
198
|
-
delegatedAgreementIds: string[];
|
|
199
190
|
toolResults: ToolResultEntry[];
|
|
200
191
|
lastOutcome: Outcome;
|
|
201
192
|
/** Claude Agent SDK session id for resume across ticks (claude-sdk cognition only). */
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ziggs-ai/agent-sdk",
|
|
3
|
-
"version": "0.10.
|
|
3
|
+
"version": "0.10.1",
|
|
4
4
|
"description": "Agent framework SDK for building autonomous agents on the Ziggs platform",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
"@anthropic-ai/claude-agent-sdk": "^0.3.204",
|
|
27
27
|
"@anthropic-ai/sdk": "^0.110.0",
|
|
28
28
|
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
29
|
-
"@ziggs-ai/api-client": "^0.9.
|
|
29
|
+
"@ziggs-ai/api-client": "^0.9.1",
|
|
30
30
|
"ajv": "^8.17.1",
|
|
31
31
|
"dotenv": "^17.2.3",
|
|
32
32
|
"openai": "^4.0.0",
|
|
@@ -1,57 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Launcher startup backlog policy (ZIG-910).
|
|
3
|
-
*
|
|
4
|
-
* "Work waits" has no statute of limitations on the backend — the inbox never
|
|
5
|
-
* expires anything. But when a fleet comes up after a dark period, replaying
|
|
6
|
-
* every weeks-old actionable slice means a mass of LLM turns on stale work at
|
|
7
|
-
* real token cost. The consumer chooses what to pick up: this gate keeps the
|
|
8
|
-
* operator-poll lazy start from resurrecting agents whose newest actionable
|
|
9
|
-
* item is older than a cutoff. Skips are never silent — one loud aggregated
|
|
10
|
-
* summary line announces what was left down and how to opt back in.
|
|
11
|
-
*
|
|
12
|
-
* Env:
|
|
13
|
-
* FLEET_REPLAY_MAX_AGE cutoff, e.g. "3d" | "12h" | "30m" | bare number =
|
|
14
|
-
* days (default "3d")
|
|
15
|
-
* FLEET_REPLAY_STALE=1 replay everything (disable the gate)
|
|
16
|
-
*
|
|
17
|
-
* Scope is the launcher only. The backend inbox is untouched, explicit
|
|
18
|
-
* `pool.startAgent()` / preStart are untouched, and an agent with ANY fresh item
|
|
19
|
-
* starts normally (its own inbox loop then drains the stale backlog too).
|
|
20
|
-
*/
|
|
21
|
-
import type { InboxOperatorAgentEntry } from '@ziggs-ai/api-client';
|
|
22
|
-
export declare const DEFAULT_REPLAY_MAX_AGE_MS: number;
|
|
23
|
-
export interface ReplayPolicy {
|
|
24
|
-
/** Skip slices whose newest actionable item is older than this. */
|
|
25
|
-
maxAgeMs: number;
|
|
26
|
-
/** FLEET_REPLAY_STALE=1 — replay everything, gate disabled. */
|
|
27
|
-
replayAll: boolean;
|
|
28
|
-
}
|
|
29
|
-
/**
|
|
30
|
-
* "3d" | "12h" | "30m" | "45s"; a bare number means days (the knob's natural
|
|
31
|
-
* granularity). Returns null on garbage so the caller can warn and fall back.
|
|
32
|
-
*/
|
|
33
|
-
export declare function parseReplayMaxAge(raw: string): number | null;
|
|
34
|
-
export declare function resolveReplayPolicy(env?: NodeJS.ProcessEnv): ReplayPolicy;
|
|
35
|
-
/**
|
|
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.
|
|
43
|
-
*/
|
|
44
|
-
export declare function newestActionableAt(entry: InboxOperatorAgentEntry): number | null;
|
|
45
|
-
export declare function formatAge(ms: number): string;
|
|
46
|
-
export interface StaleBacklogGateOptions extends ReplayPolicy {
|
|
47
|
-
label?: string;
|
|
48
|
-
now?: () => number;
|
|
49
|
-
log?: (line: string) => void;
|
|
50
|
-
}
|
|
51
|
-
/**
|
|
52
|
-
* Build the `shouldLazyStart` gate for the operator poll. Skipped agents
|
|
53
|
-
* resurface in every envelope (reading never acks), so announcements are
|
|
54
|
-
* per-agent-once: each poll pass that skips agents not yet announced emits one
|
|
55
|
-
* aggregated warn line covering them.
|
|
56
|
-
*/
|
|
57
|
-
export declare function createStaleBacklogGate({ maxAgeMs, replayAll, label, now, log, }: StaleBacklogGateOptions): (entry: InboxOperatorAgentEntry) => boolean;
|