@ziggs-ai/agent-sdk 0.9.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 +53 -14
- package/dist/AgentHost.js +137 -8
- package/dist/agent/ClaudeSdkAgent.d.ts +27 -2
- package/dist/agent/ClaudeSdkAgent.js +151 -78
- package/dist/agent/claudeSdkTools.js +39 -11
- package/dist/context/applyEffects.js +4 -3
- package/dist/defineAgent.js +4 -1
- package/dist/formatters/HistoryFormatter.js +11 -0
- package/dist/index.d.ts +3 -2
- package/dist/index.js +3 -2
- package/dist/ingress/normalizeIncoming.d.ts +5 -0
- package/dist/ingress/normalizeIncoming.js +23 -1
- package/dist/pricing/fleetDefaults.d.ts +0 -3
- package/dist/pricing/fleetDefaults.js +0 -6
- package/dist/runtime/PromptBuilder.js +9 -5
- 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/runTurn.js +1 -1
- package/dist/runtime/taskBookkeeping.d.ts +0 -2
- package/dist/runtime/taskBookkeeping.js +0 -6
- package/dist/runtime/taskIntake.js +6 -4
- package/dist/runtime/wakeWriteKeys.js +23 -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 +21 -11
- 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/tasks/TaskService.d.ts +2 -11
- package/dist/server/tools/tier1/protocolRunner.d.ts +3 -1
- package/dist/server/tools/tier1/protocolRunner.js +40 -41
- 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/tools/capabilityAdapter.js +12 -1
- package/dist/tools/recordReport.js +8 -4
- package/dist/types.d.ts +24 -11
- package/package.json +2 -2
- package/dist/server/staleBacklogGate.d.ts +0 -57
- package/dist/server/staleBacklogGate.js +0 -100
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { query } from '@anthropic-ai/claude-agent-sdk';
|
|
2
|
-
import { outcomeFromEvent } from '../runtime/buildOutcome.js';
|
|
2
|
+
import { isMessageAddressedElsewhere, outcomeFromEvent } from '../runtime/buildOutcome.js';
|
|
3
3
|
import { getBatchEvents } from '../context/batch.js';
|
|
4
4
|
import { bootstrapCtx } from '../runtime/bootstrapCtx.js';
|
|
5
5
|
import { createWakeWriteKeys } from '../runtime/wakeWriteKeys.js';
|
|
@@ -20,12 +20,18 @@ import { runtimeLog } from '../shared/runtimeLog.js';
|
|
|
20
20
|
* `notification` only.
|
|
21
21
|
*
|
|
22
22
|
* Empty / missing batch → notice-only (do not burn a turn on a malformed wake).
|
|
23
|
+
*
|
|
24
|
+
* A message addressed to a DIFFERENT party is notice-only too (attention
|
|
25
|
+
* gate): the sender named who should act, and it wasn't this agent. The
|
|
26
|
+
* message still lands in chat history and arrives as context on the next
|
|
27
|
+
* real wake — skipped here is the LLM turn, not the information.
|
|
23
28
|
*/
|
|
24
|
-
export function isNoticeOnlyWake(event) {
|
|
29
|
+
export function isNoticeOnlyWake(event, ownAgentId) {
|
|
25
30
|
const events = getBatchEvents(event);
|
|
26
31
|
if (events.length === 0)
|
|
27
32
|
return true;
|
|
28
|
-
return events.every((ev) => ev?.type === 'notification'
|
|
33
|
+
return events.every((ev) => ev?.type === 'notification' ||
|
|
34
|
+
isMessageAddressedElsewhere(ev, ownAgentId));
|
|
29
35
|
}
|
|
30
36
|
const DEFAULT_STATE = 'ready';
|
|
31
37
|
/**
|
|
@@ -62,6 +68,20 @@ const MAX_WAKES_PER_WINDOW = Number(process.env.CLAUDE_SDK_MAX_WAKES_PER_WINDOW
|
|
|
62
68
|
* discard. Env-overridable. Kept below MAX_TASK_INPUT_TOKENS so a lane resets
|
|
63
69
|
* before it can approach the per-wake abort. */
|
|
64
70
|
const MAX_SESSION_RESUME_INPUT_TOKENS = Number(process.env.CLAUDE_SDK_MAX_SESSION_INPUT_TOKENS ?? 200_000);
|
|
71
|
+
/**
|
|
72
|
+
* ZIG-1191 — "the transcript you asked me to resume is not here".
|
|
73
|
+
*
|
|
74
|
+
* Claude Code answers a `resume` of an unknown id with this, and it is the only
|
|
75
|
+
* failure that a fresh session reliably fixes: the id is valid-looking but its
|
|
76
|
+
* transcript lived on a container that is gone. Matched on the message because
|
|
77
|
+
* that is all the CLI gives us — no code, no typed error — so keep it narrow
|
|
78
|
+
* enough that a different failure never claims the retry.
|
|
79
|
+
*/
|
|
80
|
+
export function isMissingSessionError(err) {
|
|
81
|
+
const message = err?.message;
|
|
82
|
+
return (typeof message === 'string' &&
|
|
83
|
+
/no conversation found with session id/i.test(message));
|
|
84
|
+
}
|
|
65
85
|
/** Total billable input for one API response (base + cache read + cache write). */
|
|
66
86
|
function billableInput(usage) {
|
|
67
87
|
if (!usage)
|
|
@@ -155,7 +175,7 @@ function resolveOnBehalfAgreementId(rawEvent, contextSnap, agentId, sessionId) {
|
|
|
155
175
|
export class ClaudeSdkAgent {
|
|
156
176
|
workflow;
|
|
157
177
|
toolManager;
|
|
158
|
-
|
|
178
|
+
rolePrompt;
|
|
159
179
|
specialization;
|
|
160
180
|
model;
|
|
161
181
|
anthropicKey;
|
|
@@ -164,6 +184,13 @@ export class ClaudeSdkAgent {
|
|
|
164
184
|
maxTurns;
|
|
165
185
|
statelessPerWake;
|
|
166
186
|
queryFn;
|
|
187
|
+
/**
|
|
188
|
+
* ZIG-1120 #7 — tool set is fixed at host construction; rebuilding the
|
|
189
|
+
* in-process MCP server (+ zod schema tree) every wake was pure waste.
|
|
190
|
+
* A mutable holder supplies per-wake context without closing over `tick`.
|
|
191
|
+
*/
|
|
192
|
+
toolContextHolder = { current: null };
|
|
193
|
+
sdkMcpServer = null;
|
|
167
194
|
constructor(opts) {
|
|
168
195
|
if (!opts.workflow?.states)
|
|
169
196
|
throw new Error('ClaudeSdkAgent: workflow with states is required');
|
|
@@ -173,7 +200,10 @@ export class ClaudeSdkAgent {
|
|
|
173
200
|
throw new Error('ClaudeSdkAgent: anthropicKey is required');
|
|
174
201
|
this.workflow = opts.workflow;
|
|
175
202
|
this.toolManager = opts.toolManager;
|
|
176
|
-
|
|
203
|
+
// ZIG-1080: the prompt and the store blurb are two different strings. Agents
|
|
204
|
+
// written before the split still carry the prompt in `description`, so fall
|
|
205
|
+
// back to it rather than silently dropping their role.
|
|
206
|
+
this.rolePrompt = opts.systemPrompt || opts.description;
|
|
177
207
|
this.specialization = opts.specialization ?? null;
|
|
178
208
|
this.model = opts.model;
|
|
179
209
|
this.anthropicKey = opts.anthropicKey;
|
|
@@ -206,7 +236,7 @@ export class ClaudeSdkAgent {
|
|
|
206
236
|
};
|
|
207
237
|
}
|
|
208
238
|
nextCtx.recentWakes = [...recentWakes, now];
|
|
209
|
-
if (isNoticeOnlyWake(rawEvent)) {
|
|
239
|
+
if (isNoticeOnlyWake(rawEvent, identity.agentId)) {
|
|
210
240
|
runtimeLog.info('ClaudeSdkAgent', `notice-only wake — skipping LLM session=${identity.sessionId} agent=${this.agentId} ` +
|
|
211
241
|
`type=${rawEvent?.type ?? 'unknown'}`);
|
|
212
242
|
return {
|
|
@@ -219,9 +249,13 @@ export class ClaudeSdkAgent {
|
|
|
219
249
|
}
|
|
220
250
|
let contextSnap = null;
|
|
221
251
|
try {
|
|
252
|
+
// ZIG-1120 #5: pre-read is for wake gating + summarizeContext (a count) +
|
|
253
|
+
// onBehalf agreement resolution — same 12-message cap as the tool path.
|
|
254
|
+
// Uncapped pulls burned 35–47k input tokens/wake on long chats.
|
|
222
255
|
contextSnap = await eff({
|
|
223
256
|
kind: 'read-context',
|
|
224
257
|
sessionId: identity.sessionId,
|
|
258
|
+
opts: { maxMessages: 12 },
|
|
225
259
|
});
|
|
226
260
|
}
|
|
227
261
|
catch (err) {
|
|
@@ -259,7 +293,7 @@ export class ClaudeSdkAgent {
|
|
|
259
293
|
}
|
|
260
294
|
const roleBlock = [
|
|
261
295
|
`## Your role`,
|
|
262
|
-
this.
|
|
296
|
+
this.rolePrompt,
|
|
263
297
|
this.specialization ? `Specialization: ${this.specialization}` : '',
|
|
264
298
|
].filter(Boolean).join('\n');
|
|
265
299
|
const systemPrompt = `${CLAUDE_SDK_PROTOCOL}\n\n${roleBlock}`;
|
|
@@ -284,13 +318,22 @@ export class ClaudeSdkAgent {
|
|
|
284
318
|
runId,
|
|
285
319
|
wakeKeys,
|
|
286
320
|
};
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
321
|
+
this.toolContextHolder.current = toolContext;
|
|
322
|
+
if (!this.sdkMcpServer) {
|
|
323
|
+
const availableTools = await this.toolManager.getAvailableTools();
|
|
324
|
+
this.sdkMcpServer = buildZiggsSdkMcpServer(availableTools, () => {
|
|
325
|
+
const ctx = this.toolContextHolder.current;
|
|
326
|
+
if (!ctx) {
|
|
327
|
+
throw new Error('ClaudeSdkAgent: tool context missing for in-process MCP call');
|
|
328
|
+
}
|
|
329
|
+
return ctx;
|
|
330
|
+
}, {
|
|
331
|
+
name: 'ziggs',
|
|
332
|
+
instructions: 'In-process Ziggs tools backed by api-client. No external MCP process.',
|
|
333
|
+
});
|
|
334
|
+
}
|
|
292
335
|
const prompt = buildClaudeEventPrompt(rawEvent, summarizeContext(contextSnap));
|
|
293
|
-
const mcpServers = { ziggs:
|
|
336
|
+
const mcpServers = { ziggs: this.sdkMcpServer };
|
|
294
337
|
assertHostedAgentZiggsTools(mcpServers);
|
|
295
338
|
const queryOptions = claudeQueryOptions({
|
|
296
339
|
systemPrompt,
|
|
@@ -303,80 +346,110 @@ export class ClaudeSdkAgent {
|
|
|
303
346
|
if (line)
|
|
304
347
|
runtimeLog.warn('ClaudeSdkAgent', `stderr session=${identity.sessionId}: ${line}`);
|
|
305
348
|
},
|
|
306
|
-
// ZIG-860: stateless-per-wake never resumes — each wake is a fresh
|
|
307
|
-
// transcript, so per-wake input stays flat instead of replaying the
|
|
308
|
-
// whole lane history every turn.
|
|
309
|
-
resume: this.statelessPerWake ? undefined : nextCtx.claudeSessionId,
|
|
310
349
|
});
|
|
311
350
|
let metrics = { llmCalls: 0, tokens: { total: 0, input: 0, output: 0, cacheRead: 0, cacheCreation: 0 } };
|
|
312
351
|
let sessionId = nextCtx.claudeSessionId;
|
|
313
352
|
let spentInput = 0;
|
|
314
|
-
//
|
|
315
|
-
//
|
|
316
|
-
//
|
|
317
|
-
|
|
318
|
-
const
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
353
|
+
// ZIG-860: stateless-per-wake never resumes — each wake is a fresh
|
|
354
|
+
// transcript, so per-wake input stays flat instead of replaying the whole
|
|
355
|
+
// lane history every turn.
|
|
356
|
+
const resumeId = this.statelessPerWake ? undefined : nextCtx.claudeSessionId;
|
|
357
|
+
const consumeQuery = async (resume) => {
|
|
358
|
+
// Host watchdog abort (see TickInput.signal): stop consuming AND interrupt
|
|
359
|
+
// the query, so a timed-out wake never lingers as a token-burning zombie
|
|
360
|
+
// racing its own redelivered retry. Breaking the for-await returns the
|
|
361
|
+
// generator; interrupt() additionally stops a query blocked mid-turn.
|
|
362
|
+
const query = this.queryFn({ prompt, options: { ...queryOptions, resume } });
|
|
363
|
+
const onAbort = () => {
|
|
364
|
+
void query.interrupt?.()?.catch(() => { });
|
|
365
|
+
};
|
|
366
|
+
if (signal?.aborted)
|
|
367
|
+
onAbort();
|
|
368
|
+
else
|
|
369
|
+
signal?.addEventListener('abort', onAbort, { once: true });
|
|
370
|
+
try {
|
|
371
|
+
for await (const message of query) {
|
|
372
|
+
if (signal?.aborted) {
|
|
373
|
+
runtimeLog.warn('ClaudeSdkAgent', `host watchdog aborted wake session=${identity.sessionId} agent=${this.agentId} — stopping query`);
|
|
374
|
+
break;
|
|
375
|
+
}
|
|
376
|
+
if ('session_id' in message && typeof message.session_id === 'string') {
|
|
377
|
+
sessionId = message.session_id;
|
|
378
|
+
}
|
|
379
|
+
// Surface the model's own prose so a hired Claude agent is as legible as
|
|
380
|
+
// an FSM agent (ZIG-787). Each completed assistant turn's text is recorded
|
|
381
|
+
// as a `thought` — the SAME agent-private channel the FSM uses for its
|
|
382
|
+
// reasoning (runTurn `emitThought` → record-event → recordThought), so it
|
|
383
|
+
// shows in the hired-agent view without cluttering the counterparty chat.
|
|
384
|
+
// Pure tool-use turns carry no text and record nothing.
|
|
385
|
+
//
|
|
386
|
+
// Fire-and-forget on purpose: this runs inside `for await (query())`, so
|
|
387
|
+
// awaiting an artifacts round-trip per turn would couple model-stream
|
|
388
|
+
// consumption to the artifacts backend — a slow/wedged backend would stall
|
|
389
|
+
// the whole wake, not just fail a breadcrumb. Recording is best-effort and
|
|
390
|
+
// thought ordering is not load-bearing, so we let it run in the background
|
|
391
|
+
// and swallow failures with a warn.
|
|
392
|
+
if (message.type === 'assistant') {
|
|
393
|
+
const text = extractAssistantText(message).trim();
|
|
394
|
+
if (text) {
|
|
395
|
+
// Ordinal drawn synchronously so the fire-and-forget below can't
|
|
396
|
+
// reorder key assignment against other writes of this wake.
|
|
397
|
+
const idempotencyKey = wakeKeys.artifactKey({ kind: 'thought', text });
|
|
398
|
+
void eff({ kind: 'record-event', sessionId: identity.sessionId, entry: { kind: 'thought', text }, idempotencyKey })
|
|
399
|
+
.catch((err) => runtimeLog.warn('ClaudeSdkAgent', `record thought failed session=${identity.sessionId}: ${err.message}`));
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
const m = extractMetrics(message);
|
|
403
|
+
if (m)
|
|
404
|
+
metrics = m;
|
|
405
|
+
if (message.type === 'result' && message.subtype !== 'success') {
|
|
406
|
+
runtimeLog.warn('ClaudeSdkAgent', `query ended without success subtype=${message.subtype} session=${identity.sessionId}`);
|
|
407
|
+
}
|
|
408
|
+
// Runaway guard (ZIG-759): abort before an unbounded loop drains the
|
|
409
|
+
// shared fleet key. Breaking the for-await calls the generator's return(),
|
|
410
|
+
// which terminates the underlying query.
|
|
411
|
+
spentInput += billableInput(usageOf(message));
|
|
412
|
+
if (spentInput > MAX_TASK_INPUT_TOKENS) {
|
|
413
|
+
runtimeLog.warn('ClaudeSdkAgent', `input-token budget exceeded (~${spentInput} > ${MAX_TASK_INPUT_TOKENS}) session=${identity.sessionId} agent=${this.agentId} — aborting query to cap spend (ZIG-759)`);
|
|
414
|
+
break;
|
|
356
415
|
}
|
|
357
416
|
}
|
|
358
|
-
const m = extractMetrics(message);
|
|
359
|
-
if (m)
|
|
360
|
-
metrics = m;
|
|
361
|
-
if (message.type === 'result' && message.subtype !== 'success') {
|
|
362
|
-
runtimeLog.warn('ClaudeSdkAgent', `query ended without success subtype=${message.subtype} session=${identity.sessionId}`);
|
|
363
|
-
}
|
|
364
|
-
// Runaway guard (ZIG-759): abort before an unbounded loop drains the
|
|
365
|
-
// shared fleet key. Breaking the for-await calls the generator's return(),
|
|
366
|
-
// which terminates the underlying query.
|
|
367
|
-
spentInput += billableInput(usageOf(message));
|
|
368
|
-
if (spentInput > MAX_TASK_INPUT_TOKENS) {
|
|
369
|
-
runtimeLog.warn('ClaudeSdkAgent', `input-token budget exceeded (~${spentInput} > ${MAX_TASK_INPUT_TOKENS}) session=${identity.sessionId} agent=${this.agentId} — aborting query to cap spend (ZIG-759)`);
|
|
370
|
-
break;
|
|
371
|
-
}
|
|
372
417
|
}
|
|
418
|
+
finally {
|
|
419
|
+
signal?.removeEventListener('abort', onAbort);
|
|
420
|
+
}
|
|
421
|
+
};
|
|
422
|
+
try {
|
|
423
|
+
await consumeQuery(resumeId);
|
|
373
424
|
}
|
|
374
425
|
catch (err) {
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
426
|
+
// ZIG-1191: the resumed transcript is gone. Claude Code stores transcripts
|
|
427
|
+
// on the container's own disk while the session id outlives it — persisted
|
|
428
|
+
// per lane in the session store with a 24h TTL — so a redeployed agents
|
|
429
|
+
// task inherits an id pointing at a file that no longer exists. Every wake
|
|
430
|
+
// on that lane then failed identically for as long as the TTL held, and the
|
|
431
|
+
// human saw one "Something went wrong. Please try again." per attempt.
|
|
432
|
+
//
|
|
433
|
+
// Retrying here is safe precisely because this failure happens BEFORE any
|
|
434
|
+
// turn runs: nothing was emitted, no tool ran, no idempotency ordinal was
|
|
435
|
+
// spent. That is why the retry is scoped to this one error and to one
|
|
436
|
+
// attempt — a mid-wake failure must keep propagating, since its partial
|
|
437
|
+
// writes are already out.
|
|
438
|
+
if (!isMissingSessionError(err) || !resumeId) {
|
|
439
|
+
runtimeLog.warn('ClaudeSdkAgent', `query failed session=${identity.sessionId}: ${err.message}`);
|
|
440
|
+
throw err;
|
|
441
|
+
}
|
|
442
|
+
runtimeLog.info('ClaudeSdkAgent', `resumed Claude session ${resumeId} no longer exists (container replaced?) ` +
|
|
443
|
+
`session=${identity.sessionId} agent=${this.agentId} — retrying once on a fresh session (ZIG-1191)`);
|
|
444
|
+
nextCtx.claudeSessionId = undefined;
|
|
445
|
+
sessionId = undefined;
|
|
446
|
+
try {
|
|
447
|
+
await consumeQuery(undefined);
|
|
448
|
+
}
|
|
449
|
+
catch (retryErr) {
|
|
450
|
+
runtimeLog.warn('ClaudeSdkAgent', `query failed session=${identity.sessionId}: ${retryErr.message}`);
|
|
451
|
+
throw retryErr;
|
|
452
|
+
}
|
|
380
453
|
}
|
|
381
454
|
// Stateless-per-wake keeps no session id — clear any carried over from the
|
|
382
455
|
// prior ctx so the next wake starts fresh too (ZIG-860). Otherwise remember
|
|
@@ -8,18 +8,37 @@ import { listGrantsTool } from '../server/tools/tier2/grantsTools.js';
|
|
|
8
8
|
// messages cover "what woke me and what was just said"; the model can pass a
|
|
9
9
|
// larger maxMessages explicitly when it genuinely needs deeper history.
|
|
10
10
|
const DEFAULT_SNAPSHOT_MAX_MESSAGES = 12;
|
|
11
|
+
/**
|
|
12
|
+
* First usable string among the candidates, treating blank as absent.
|
|
13
|
+
*
|
|
14
|
+
* ZIG-1081: a model that does not know a value fills the field with `""` rather
|
|
15
|
+
* than omitting it — the schema shows `chatId`, the agent has no way to know it,
|
|
16
|
+
* so it sends an empty string. Plain `??` let that blank win over the lane
|
|
17
|
+
* default, and the very next line rejected it as missing. Every wake opened with
|
|
18
|
+
* a failed `context_snapshot` and a recovery turn.
|
|
19
|
+
*/
|
|
20
|
+
function firstFilled(...candidates) {
|
|
21
|
+
for (const candidate of candidates) {
|
|
22
|
+
if (typeof candidate === 'string' && candidate.trim() !== '')
|
|
23
|
+
return candidate;
|
|
24
|
+
}
|
|
25
|
+
return undefined;
|
|
26
|
+
}
|
|
11
27
|
// Named distinctly from ziggs-mcp's per-type `ziggs_context_read`: this is the
|
|
12
28
|
// one-shot aggregate snapshot (grant-fenced server-side), not a cursored read.
|
|
13
29
|
export const contextSnapshotTool = defineTool('context_snapshot', {
|
|
14
|
-
chatId: { type: 'string', description: '
|
|
30
|
+
chatId: { type: 'string', description: 'Omit this to snapshot the chat you were woken in — that is the usual case, and you do not need to know its id. Only pass a chatId to snapshot a DIFFERENT chat.' },
|
|
15
31
|
maxMessages: { type: 'number', description: `Message history cap (default ${DEFAULT_SNAPSHOT_MAX_MESSAGES}). Pass a larger value when you need deeper history.` },
|
|
16
32
|
}, async (args, ctx) => {
|
|
17
33
|
const c = ctx;
|
|
18
34
|
if (!c.eff)
|
|
19
35
|
throw new Error('context_snapshot requires eff in tool context');
|
|
20
|
-
const sessionId = args.chatId
|
|
21
|
-
if (!sessionId)
|
|
22
|
-
throw new Error('context_snapshot:
|
|
36
|
+
const sessionId = firstFilled(args.chatId, c.chatId, c.sessionId);
|
|
37
|
+
if (!sessionId) {
|
|
38
|
+
throw new Error('context_snapshot: this wake carries no chat to snapshot. Call it with no arguments ' +
|
|
39
|
+
'to read the chat you were woken in; if this wake is on an agreement lane with no ' +
|
|
40
|
+
'single chat, read the agreement instead and pass a chatId from one of its chats.');
|
|
41
|
+
}
|
|
23
42
|
return c.eff({
|
|
24
43
|
kind: 'read-context',
|
|
25
44
|
sessionId,
|
|
@@ -29,22 +48,24 @@ export const contextSnapshotTool = defineTool('context_snapshot', {
|
|
|
29
48
|
});
|
|
30
49
|
}, { description: 'Read the chat snapshot: the NEWEST maxMessages of history (chronological order), plus agreements, agents, and users for a session.' });
|
|
31
50
|
export const chatSendTool = defineTool('chat_send', {
|
|
32
|
-
chatId: { type: 'string', description: '
|
|
33
|
-
receiverId: { type: 'string', description: 'Recipient user or agent id.
|
|
51
|
+
chatId: { type: 'string', description: 'Omit this to send in the chat you were woken in — that is the usual case. Only pass a chatId to send in a DIFFERENT chat.' },
|
|
52
|
+
receiverId: { type: 'string', description: 'Recipient user or agent id. Omit when replying to whoever woke you — that is the default.' },
|
|
34
53
|
text: { type: 'string', required: true },
|
|
35
54
|
idempotencyKey: { type: 'string', description: 'Reuse for retry-safe resend of the same logical message.' },
|
|
36
55
|
}, async (args, ctx) => {
|
|
37
56
|
const c = ctx;
|
|
38
57
|
if (!c.eff)
|
|
39
58
|
throw new Error('chat_send requires eff in tool context');
|
|
40
|
-
const sessionId = args.chatId
|
|
41
|
-
if (!sessionId)
|
|
42
|
-
throw new Error('chat_send: chatId
|
|
59
|
+
const sessionId = firstFilled(args.chatId, c.chatId, c.sessionId);
|
|
60
|
+
if (!sessionId) {
|
|
61
|
+
throw new Error('chat_send: this wake carries no chat to send in. Call it with no chatId to reply in ' +
|
|
62
|
+
'the chat you were woken in, or pass the chatId of the chat you mean.');
|
|
63
|
+
}
|
|
43
64
|
// Replying to whoever woke you is the overwhelmingly common case; making
|
|
44
65
|
// the model spell it out cost a failed tool call + a full extra LLM turn
|
|
45
66
|
// per reply. Batch/timer wakes carry no sender — those still require an
|
|
46
67
|
// explicit receiverId.
|
|
47
|
-
const receiverId = args.receiverId
|
|
68
|
+
const receiverId = firstFilled(args.receiverId, c.senderId);
|
|
48
69
|
if (!receiverId)
|
|
49
70
|
throw new Error('chat_send: receiverId is required (this wake has no sender to default to)');
|
|
50
71
|
// Default to the wake's content-derived key (ZIG-1045): a crash-replay that
|
|
@@ -99,12 +120,19 @@ export const listTasksTool = defineTool('task_list', {
|
|
|
99
120
|
if (!c.agentId)
|
|
100
121
|
throw new Error('task_list: agentId missing from tool context');
|
|
101
122
|
const assignedTo = args.assignedToMe ? c.agentId : args.assignedTo;
|
|
123
|
+
// ZIG-1092: the lane fences this to the engagement's tasks. Without it the
|
|
124
|
+
// list is every task the agent's key can reach, across all its customers.
|
|
125
|
+
const laneId = c.chatId || c.sessionId || undefined;
|
|
102
126
|
return listTasks({
|
|
103
127
|
state: args.state,
|
|
104
128
|
cursor: args.cursor,
|
|
105
129
|
limit: typeof args.limit === 'number' ? args.limit : undefined,
|
|
106
130
|
assignedTo,
|
|
107
|
-
}, {
|
|
131
|
+
}, {
|
|
132
|
+
operatorKey: c.operatorKey,
|
|
133
|
+
agentId: c.agentId,
|
|
134
|
+
...(laneId ? { laneId } : {}),
|
|
135
|
+
});
|
|
108
136
|
}, {
|
|
109
137
|
description: 'List tasks reachable by this agent (GET /tasks). Scope follows the operator key — same reach as chats and agreements. Optional state / assignee filters and cursor pagination. Use assignedToMe to list your own open work.',
|
|
110
138
|
});
|
|
@@ -21,10 +21,11 @@ function applyTaskOperationResultToContext(updates, operation, result) {
|
|
|
21
21
|
}
|
|
22
22
|
}
|
|
23
23
|
if (operation === 'task-update') {
|
|
24
|
-
|
|
25
|
-
|
|
24
|
+
// `state` only — ZIG-1057 cut `status` from the wire (ZIG-1128).
|
|
25
|
+
const state = (task?.['state'] || r['state']);
|
|
26
|
+
if (state === 'completed')
|
|
26
27
|
updates['taskCompleted'] = true;
|
|
27
|
-
if (
|
|
28
|
+
if (state === 'failed')
|
|
28
29
|
updates['taskFailed'] = true;
|
|
29
30
|
}
|
|
30
31
|
if (operation === 'agreement-respond')
|
package/dist/defineAgent.js
CHANGED
|
@@ -35,7 +35,10 @@ export function defineAgent(input) {
|
|
|
35
35
|
// states/transitions are validated too.
|
|
36
36
|
const workflow = injectTaskIntakeDefault(injectKickoffDefault({
|
|
37
37
|
id: input.agentId,
|
|
38
|
-
description
|
|
38
|
+
// The workflow description is the agent's role text, which PromptBuilder
|
|
39
|
+
// renders into <self>. ZIG-1080 split that from the store-facing
|
|
40
|
+
// `description`: prefer the prompt when the author supplied one.
|
|
41
|
+
description: input.systemPrompt || input.description,
|
|
39
42
|
initial: input.initial,
|
|
40
43
|
states: input.states,
|
|
41
44
|
}));
|
|
@@ -126,6 +126,17 @@ export class HistoryFormatter {
|
|
|
126
126
|
return 'System';
|
|
127
127
|
if (id === agentId)
|
|
128
128
|
return 'You';
|
|
129
|
+
// ZIG-1137: prefer presentation face when the wire masks the principal.
|
|
130
|
+
const presentation = participant['presentation'];
|
|
131
|
+
const faceName = (typeof presentation?.['name'] === 'string' && presentation['name']) ||
|
|
132
|
+
(typeof presentation?.['persona']?.['name'] === 'string'
|
|
133
|
+
? presentation['persona']['name']
|
|
134
|
+
: null);
|
|
135
|
+
if (faceName)
|
|
136
|
+
return `${faceName} (${id})`;
|
|
137
|
+
if (id.startsWith('rpb_') || id.startsWith('psn_')) {
|
|
138
|
+
return `${id} (presentation)`;
|
|
139
|
+
}
|
|
129
140
|
return `${id} (${participant['type'] || 'unknown'})`;
|
|
130
141
|
}
|
|
131
142
|
formatOwner(task, agentId) {
|
package/dist/index.d.ts
CHANGED
|
@@ -41,7 +41,6 @@ export { default } from './AgentHost.js';
|
|
|
41
41
|
export { validateContext, ContextWarning } from './cognition/validateContext.js';
|
|
42
42
|
export { runLauncher } from './server/runLauncher.js';
|
|
43
43
|
export { ConnectionPool } from './server/ConnectionPool.js';
|
|
44
|
-
export { createStaleBacklogGate, resolveReplayPolicy, newestActionableAt, parseReplayMaxAge, } from './server/staleBacklogGate.js';
|
|
45
44
|
export { decodeOperatorKeyClaims, boundKeyFleetWarning } from './shared/operatorKeyClaims.js';
|
|
46
45
|
export { createHealthServer } from './server/createHealthServer.js';
|
|
47
46
|
export { TaskService } from './server/tasks/TaskService.js';
|
|
@@ -55,6 +54,8 @@ export { OpenAIAdapter } from './adapters/OpenAIAdapter.js';
|
|
|
55
54
|
export { extractJSON, safeParseJSON } from './utils/jsonExtractor.js';
|
|
56
55
|
export { HistoryFormatter, historyFormatter, AgreementFormatter, agreementFormatter } from './formatters/index.js';
|
|
57
56
|
export { ProactiveTrigger } from './server/proactive/ProactiveTrigger.js';
|
|
57
|
+
export { PollRegistry, declaredPolls, type PollBinding, type PollContext, type PollKind, type PollResult, } from './server/polls/PollKind.js';
|
|
58
|
+
export { PollRunner, type PollRunnerOptions, type WakeTarget } from './server/polls/PollRunner.js';
|
|
58
59
|
export { PROTOCOL_TOOLS, agreementProposeTool, agreementSubcontractTool, agreementRespondTool, agreementClaimTool, agreementCounterTool, agreementGetTool, agreementRevokeTool, agreementFulfillTool, taskCreateTool, taskSetResultTool, taskUpdatePlanStepTool, dispatchProtocolOp, PAYMENT_TOOLS, paymentBalanceTool, paymentTransferTool, paymentHoldTool, paymentReleaseTool, paymentResolveWalletTool, CONNECTION_TOOLS, connectionProxyTool, connectionListGrantsTool, requestConnectionTool, DISCOVERY_TOOLS, agentSearchTool, agentGetTool, MARKETPLACE_TOOLS, marketplaceViewTool, CONTEXT_TOOLS, contextReadTool, contextDelegateTool, contextExpandReachTool, contextDiscoverGrantableTool, GRANTS_TOOLS, listGrantsTool, LINK_TOOLS, linkCreateInviteTool, linkListTool, ARTIFACT_TOOLS, recordArtifactTool, CHAT_TOOLS, openConversationTool, } from './server/tools/index.js';
|
|
59
60
|
export { CLAUDE_SDK_TOOLS } from './agent/claudeSdkTools.js';
|
|
60
61
|
export { PROTOCOL_TOOL_NAMES, PROTOCOL_TOOL_TO_OPERATION, mapProtocolToolToOperation, isProtocolToolName } from './tasks/protocolRegistry.js';
|
|
@@ -65,7 +66,7 @@ export { classifyIncomingEvent } from './context/classifyEnvelope.js';
|
|
|
65
66
|
export { buildContextUpdates, CONTEXT_RESET } from './context/applyEffects.js';
|
|
66
67
|
export { getBatchEvents, isTaskResultRelevantToAgent } from './context/batch.js';
|
|
67
68
|
export { classifyWorkflowEvent, findTaskResult, unwrapBatchEvent, findIncomingTaskResult } from './context/routingLabels.js';
|
|
68
|
-
export { MessagesClient, ArtifactsClient, AgentSearchClient, getBackendUrl,
|
|
69
|
+
export { MessagesClient, ArtifactsClient, AgentSearchClient, getBackendUrl, } from '@ziggs-ai/api-client';
|
|
69
70
|
export { SeenMessages } from './server/SeenMessages.js';
|
|
70
71
|
export { runtimeLog, resetRuntimeLogLevelCache } from './shared/runtimeLog.js';
|
|
71
72
|
export type { RuntimeLogLevel } from './shared/runtimeLog.js';
|
package/dist/index.js
CHANGED
|
@@ -35,7 +35,6 @@ export { default } from './AgentHost.js';
|
|
|
35
35
|
export { validateContext, ContextWarning } from './cognition/validateContext.js';
|
|
36
36
|
export { runLauncher } from './server/runLauncher.js';
|
|
37
37
|
export { ConnectionPool } from './server/ConnectionPool.js';
|
|
38
|
-
export { createStaleBacklogGate, resolveReplayPolicy, newestActionableAt, parseReplayMaxAge, } from './server/staleBacklogGate.js';
|
|
39
38
|
export { decodeOperatorKeyClaims, boundKeyFleetWarning } from './shared/operatorKeyClaims.js';
|
|
40
39
|
export { createHealthServer } from './server/createHealthServer.js';
|
|
41
40
|
export { TaskService } from './server/tasks/TaskService.js';
|
|
@@ -48,6 +47,8 @@ export { OpenAIAdapter } from './adapters/OpenAIAdapter.js';
|
|
|
48
47
|
export { extractJSON, safeParseJSON } from './utils/jsonExtractor.js';
|
|
49
48
|
export { HistoryFormatter, historyFormatter, AgreementFormatter, agreementFormatter } from './formatters/index.js';
|
|
50
49
|
export { ProactiveTrigger } from './server/proactive/ProactiveTrigger.js';
|
|
50
|
+
export { PollRegistry, declaredPolls, } from './server/polls/PollKind.js';
|
|
51
|
+
export { PollRunner } from './server/polls/PollRunner.js';
|
|
51
52
|
// ZIG-894 — the hand-rolled ZiggsPayClient / ZiggsConnectClient were deleted;
|
|
52
53
|
// payment and connection tools now use @ziggs-ai/api-client's PaymentsClient /
|
|
53
54
|
// ConnectionsClient, the same clients ziggs-mcp uses.
|
|
@@ -70,6 +71,6 @@ export { classifyIncomingEvent } from './context/classifyEnvelope.js';
|
|
|
70
71
|
export { buildContextUpdates, CONTEXT_RESET } from './context/applyEffects.js';
|
|
71
72
|
export { getBatchEvents, isTaskResultRelevantToAgent } from './context/batch.js';
|
|
72
73
|
export { classifyWorkflowEvent, findTaskResult, unwrapBatchEvent, findIncomingTaskResult } from './context/routingLabels.js';
|
|
73
|
-
export { MessagesClient, ArtifactsClient, AgentSearchClient, getBackendUrl,
|
|
74
|
+
export { MessagesClient, ArtifactsClient, AgentSearchClient, getBackendUrl, } from '@ziggs-ai/api-client';
|
|
74
75
|
export { SeenMessages } from './server/SeenMessages.js';
|
|
75
76
|
export { runtimeLog, resetRuntimeLogLevelCache } from './shared/runtimeLog.js';
|
|
@@ -45,6 +45,27 @@ function isAlreadyActivatedProposalCreated(metadata) {
|
|
|
45
45
|
const proposalStatus = agr.proposal?.status;
|
|
46
46
|
return status === 'active' || proposalStatus === 'approved';
|
|
47
47
|
}
|
|
48
|
+
/**
|
|
49
|
+
* ZIG-1078 — a `proposal_created` notification addressed to a party whose
|
|
50
|
+
* approval is PENDING is a consent REQUEST, not a status notice. The backend
|
|
51
|
+
* fans `proposal_created` out specifically to the pending approvers
|
|
52
|
+
* (ZIG-244), and parking it meant the hand-off provider (ZIG-1059) — or any
|
|
53
|
+
* agent whose consent a proposal awaits — was alerted and then never looked:
|
|
54
|
+
* the approval sat pending until a human nudged the chat. Same technique as
|
|
55
|
+
* isAlreadyActivatedProposalCreated (ZIG-1052): reclassify by what the
|
|
56
|
+
* embedded agreement SAYS rather than adding a new wake word.
|
|
57
|
+
*/
|
|
58
|
+
function isPendingApprovalRequestNotification(metadata) {
|
|
59
|
+
if (wireOperation(metadata) !== 'proposal_created')
|
|
60
|
+
return false;
|
|
61
|
+
const receiverId = metadata.receiver?.id;
|
|
62
|
+
if (!receiverId)
|
|
63
|
+
return false;
|
|
64
|
+
const approvals = metadata.service?.agreement?.approvals;
|
|
65
|
+
if (!Array.isArray(approvals))
|
|
66
|
+
return false;
|
|
67
|
+
return approvals.some((a) => a && a.partyId === receiverId && a.status === 'pending');
|
|
68
|
+
}
|
|
48
69
|
function isAgreementLifecycleNotification(metadata) {
|
|
49
70
|
const entryType = metadata.entryType || 'message';
|
|
50
71
|
if (entryType !== 'notification')
|
|
@@ -53,7 +74,8 @@ function isAgreementLifecycleNotification(metadata) {
|
|
|
53
74
|
if (typeof operation === 'string' && PROPOSAL_RESOLVED_OPERATIONS.has(operation)) {
|
|
54
75
|
return true;
|
|
55
76
|
}
|
|
56
|
-
return isAlreadyActivatedProposalCreated(metadata)
|
|
77
|
+
return (isAlreadyActivatedProposalCreated(metadata) ||
|
|
78
|
+
isPendingApprovalRequestNotification(metadata));
|
|
57
79
|
}
|
|
58
80
|
function isTerminalTaskState(state) {
|
|
59
81
|
return state === 'completed' || state === 'failed' || state === 'cancelled';
|
|
@@ -19,7 +19,6 @@ export interface ProtocolPricingPayload {
|
|
|
19
19
|
providerId?: string;
|
|
20
20
|
executorId?: string;
|
|
21
21
|
parentAgreementId?: string;
|
|
22
|
-
parentTaskId?: string;
|
|
23
22
|
[key: string]: unknown;
|
|
24
23
|
}
|
|
25
24
|
/**
|
|
@@ -36,8 +35,6 @@ export declare function applyFleetAgreementPricing(operation: 'agreement-propose
|
|
|
36
35
|
* out unwidened by `ProtocolPricingPayload`'s `[key: string]: unknown` index.
|
|
37
36
|
*/
|
|
38
37
|
export declare function applyFleetCounterPricing<T extends ProtocolPricingPayload>(payload: T, actingAgentId: string): T;
|
|
39
|
-
/** User or agent direct-create / marketplace: default from provider specialist. */
|
|
40
|
-
export declare function resolveDefaultPriceForProvider(providerId: string, description?: string | null, explicitPrice?: number | null): number | null;
|
|
41
38
|
/**
|
|
42
39
|
* Infer fleet actor when tool context omits agentId (ZIG-368 safety net).
|
|
43
40
|
*/
|
|
@@ -121,12 +121,6 @@ export function applyFleetCounterPricing(payload, actingAgentId) {
|
|
|
121
121
|
return payload;
|
|
122
122
|
return { ...payload, price: cents };
|
|
123
123
|
}
|
|
124
|
-
/** User or agent direct-create / marketplace: default from provider specialist. */
|
|
125
|
-
export function resolveDefaultPriceForProvider(providerId, description, explicitPrice) {
|
|
126
|
-
if (typeof explicitPrice === 'number')
|
|
127
|
-
return explicitPrice;
|
|
128
|
-
return resolveDefaultPriceCentsForExecutor(providerId, description);
|
|
129
|
-
}
|
|
130
124
|
/**
|
|
131
125
|
* Infer fleet actor when tool context omits agentId (ZIG-368 safety net).
|
|
132
126
|
*/
|