@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,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)
|
|
@@ -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');
|
|
@@ -209,7 +236,7 @@ export class ClaudeSdkAgent {
|
|
|
209
236
|
};
|
|
210
237
|
}
|
|
211
238
|
nextCtx.recentWakes = [...recentWakes, now];
|
|
212
|
-
if (isNoticeOnlyWake(rawEvent)) {
|
|
239
|
+
if (isNoticeOnlyWake(rawEvent, identity.agentId)) {
|
|
213
240
|
runtimeLog.info('ClaudeSdkAgent', `notice-only wake — skipping LLM session=${identity.sessionId} agent=${this.agentId} ` +
|
|
214
241
|
`type=${rawEvent?.type ?? 'unknown'}`);
|
|
215
242
|
return {
|
|
@@ -222,9 +249,13 @@ export class ClaudeSdkAgent {
|
|
|
222
249
|
}
|
|
223
250
|
let contextSnap = null;
|
|
224
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.
|
|
225
255
|
contextSnap = await eff({
|
|
226
256
|
kind: 'read-context',
|
|
227
257
|
sessionId: identity.sessionId,
|
|
258
|
+
opts: { maxMessages: 12 },
|
|
228
259
|
});
|
|
229
260
|
}
|
|
230
261
|
catch (err) {
|
|
@@ -287,13 +318,22 @@ export class ClaudeSdkAgent {
|
|
|
287
318
|
runId,
|
|
288
319
|
wakeKeys,
|
|
289
320
|
};
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
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
|
+
}
|
|
295
335
|
const prompt = buildClaudeEventPrompt(rawEvent, summarizeContext(contextSnap));
|
|
296
|
-
const mcpServers = { ziggs:
|
|
336
|
+
const mcpServers = { ziggs: this.sdkMcpServer };
|
|
297
337
|
assertHostedAgentZiggsTools(mcpServers);
|
|
298
338
|
const queryOptions = claudeQueryOptions({
|
|
299
339
|
systemPrompt,
|
|
@@ -306,80 +346,110 @@ export class ClaudeSdkAgent {
|
|
|
306
346
|
if (line)
|
|
307
347
|
runtimeLog.warn('ClaudeSdkAgent', `stderr session=${identity.sessionId}: ${line}`);
|
|
308
348
|
},
|
|
309
|
-
// ZIG-860: stateless-per-wake never resumes — each wake is a fresh
|
|
310
|
-
// transcript, so per-wake input stays flat instead of replaying the
|
|
311
|
-
// whole lane history every turn.
|
|
312
|
-
resume: this.statelessPerWake ? undefined : nextCtx.claudeSessionId,
|
|
313
349
|
});
|
|
314
350
|
let metrics = { llmCalls: 0, tokens: { total: 0, input: 0, output: 0, cacheRead: 0, cacheCreation: 0 } };
|
|
315
351
|
let sessionId = nextCtx.claudeSessionId;
|
|
316
352
|
let spentInput = 0;
|
|
317
|
-
//
|
|
318
|
-
//
|
|
319
|
-
//
|
|
320
|
-
|
|
321
|
-
const
|
|
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
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
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;
|
|
359
415
|
}
|
|
360
416
|
}
|
|
361
|
-
const m = extractMetrics(message);
|
|
362
|
-
if (m)
|
|
363
|
-
metrics = m;
|
|
364
|
-
if (message.type === 'result' && message.subtype !== 'success') {
|
|
365
|
-
runtimeLog.warn('ClaudeSdkAgent', `query ended without success subtype=${message.subtype} session=${identity.sessionId}`);
|
|
366
|
-
}
|
|
367
|
-
// Runaway guard (ZIG-759): abort before an unbounded loop drains the
|
|
368
|
-
// shared fleet key. Breaking the for-await calls the generator's return(),
|
|
369
|
-
// which terminates the underlying query.
|
|
370
|
-
spentInput += billableInput(usageOf(message));
|
|
371
|
-
if (spentInput > MAX_TASK_INPUT_TOKENS) {
|
|
372
|
-
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)`);
|
|
373
|
-
break;
|
|
374
|
-
}
|
|
375
417
|
}
|
|
418
|
+
finally {
|
|
419
|
+
signal?.removeEventListener('abort', onAbort);
|
|
420
|
+
}
|
|
421
|
+
};
|
|
422
|
+
try {
|
|
423
|
+
await consumeQuery(resumeId);
|
|
376
424
|
}
|
|
377
425
|
catch (err) {
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
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
|
+
}
|
|
383
453
|
}
|
|
384
454
|
// Stateless-per-wake keeps no session id — clear any carried over from the
|
|
385
455
|
// prior ctx so the next wake starts fresh too (ZIG-860). Otherwise remember
|
|
@@ -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/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';
|
|
@@ -18,14 +18,15 @@ import { migrateLegacyTaskFields } from './taskBookkeeping.js';
|
|
|
18
18
|
export function bootstrapCtx(identity, existing) {
|
|
19
19
|
if (existing) {
|
|
20
20
|
const legacy = existing;
|
|
21
|
+
// ZIG-1128: the lane-level mirrors are destructured OFF, not carried through.
|
|
22
|
+
// `migrateLegacyTaskFields` has already folded whatever they held into
|
|
23
|
+
// `tasks`; keeping the empty husks on the restored Ctx is what let a live FSM
|
|
24
|
+
// guard read one and see "nothing delegated" forever.
|
|
25
|
+
const { pendingSubtasks: _pendingSubtasks, collectedResults: _collectedResults, delegatedAgreementIds: _delegatedAgreementIds, ...rest } = existing;
|
|
21
26
|
return {
|
|
22
|
-
...
|
|
27
|
+
...rest,
|
|
23
28
|
identity,
|
|
24
29
|
tasks: migrateLegacyTaskFields(legacy),
|
|
25
|
-
// ZIG-1057: deprecated mirrors stay empty; use all*() readers.
|
|
26
|
-
pendingSubtasks: {},
|
|
27
|
-
collectedResults: [],
|
|
28
|
-
delegatedAgreementIds: [],
|
|
29
30
|
};
|
|
30
31
|
}
|
|
31
32
|
return {
|
|
@@ -34,9 +35,6 @@ export function bootstrapCtx(identity, existing) {
|
|
|
34
35
|
activeAgreementId: null,
|
|
35
36
|
activeTaskId: null,
|
|
36
37
|
pendingProposalAgreementId: null,
|
|
37
|
-
pendingSubtasks: {},
|
|
38
|
-
collectedResults: [],
|
|
39
|
-
delegatedAgreementIds: [],
|
|
40
38
|
toolResults: [],
|
|
41
39
|
lastOutcome: { kind: 'enter' },
|
|
42
40
|
};
|
|
@@ -3,7 +3,6 @@ import type { Action, AgreementRef, Ctx, Outcome, TaskRef, ToolResultEntry } fro
|
|
|
3
3
|
export interface WireResult {
|
|
4
4
|
taskId?: string;
|
|
5
5
|
state?: string;
|
|
6
|
-
status?: string;
|
|
7
6
|
agreement?: AgreementRef;
|
|
8
7
|
agreementId?: string;
|
|
9
8
|
proposal?: {
|
|
@@ -37,6 +36,15 @@ export interface EmittedEvent {
|
|
|
37
36
|
senderId?: string;
|
|
38
37
|
[key: string]: unknown;
|
|
39
38
|
}
|
|
39
|
+
/**
|
|
40
|
+
* A chat message whose stated receiver is a DIFFERENT party. `receiverId` is
|
|
41
|
+
* the sender's addressing decision, carried on every wire frame; the
|
|
42
|
+
* human-broadcast sentinel counts as "elsewhere" for an agent (a broadcast is
|
|
43
|
+
* addressed to the room's humans). A frame with NO receiver keeps the turn —
|
|
44
|
+
* we cannot know it is not ours, and skipping on absence would deafen agents
|
|
45
|
+
* to malformed or legacy frames.
|
|
46
|
+
*/
|
|
47
|
+
export declare function isMessageAddressedElsewhere(ev: RawEvent | null | undefined, ownAgentId: string | null | undefined): boolean;
|
|
40
48
|
/**
|
|
41
49
|
* Converts a raw incoming event into the single Outcome that re-enters the
|
|
42
50
|
* machine.
|
|
@@ -1,6 +1,21 @@
|
|
|
1
1
|
import { isProtocolToolName, mapProtocolToolToOperation } from '../tasks/protocolRegistry.js';
|
|
2
2
|
import { getBatchEvents, isTaskResultRelevantToAgent } from '../context/batch.js';
|
|
3
3
|
import { bookForDelegation, ensureTaskBook, findBookHoldingPending, removeTaskBook, } from './taskBookkeeping.js';
|
|
4
|
+
/**
|
|
5
|
+
* A chat message whose stated receiver is a DIFFERENT party. `receiverId` is
|
|
6
|
+
* the sender's addressing decision, carried on every wire frame; the
|
|
7
|
+
* human-broadcast sentinel counts as "elsewhere" for an agent (a broadcast is
|
|
8
|
+
* addressed to the room's humans). A frame with NO receiver keeps the turn —
|
|
9
|
+
* we cannot know it is not ours, and skipping on absence would deafen agents
|
|
10
|
+
* to malformed or legacy frames.
|
|
11
|
+
*/
|
|
12
|
+
export function isMessageAddressedElsewhere(ev, ownAgentId) {
|
|
13
|
+
return Boolean(ev &&
|
|
14
|
+
ev.type === 'message' &&
|
|
15
|
+
ownAgentId &&
|
|
16
|
+
ev.receiverId &&
|
|
17
|
+
ev.receiverId !== ownAgentId);
|
|
18
|
+
}
|
|
4
19
|
// ── outcomeFromEvent: incoming wire event → Outcome ──────────────────────────
|
|
5
20
|
/**
|
|
6
21
|
* Converts a raw incoming event into the single Outcome that re-enters the
|
|
@@ -139,6 +154,14 @@ function singleEventToOutcome(ev, ownAgentId) {
|
|
|
139
154
|
// Suppress our own messages echoed back through the chat fanout.
|
|
140
155
|
if (ownAgentId && ev.senderId && ev.senderId === ownAgentId)
|
|
141
156
|
return null;
|
|
157
|
+
// Attention gate: a message addressed to someone else is room context, not
|
|
158
|
+
// a turn. Delivery is audience-based on purpose (everyone in the chat hears
|
|
159
|
+
// everything), so in a multi-agent room every agent used to burn an LLM turn
|
|
160
|
+
// per utterance just to decide it wasn't theirs — the sender already decided
|
|
161
|
+
// that by naming a receiver. The message still lands in chat history and
|
|
162
|
+
// rides in on the next wake's read-back; nothing is lost but the interrupt.
|
|
163
|
+
if (isMessageAddressedElsewhere(ev, ownAgentId))
|
|
164
|
+
return null;
|
|
142
165
|
const text = typeof ev.text === 'string' ? ev.text : undefined;
|
|
143
166
|
const senderType = typeof ev.senderType === 'string' ? ev.senderType : undefined;
|
|
144
167
|
return { kind: 'message-received', text, senderId: ev.senderId, senderType };
|
|
@@ -286,8 +309,9 @@ function inferKindFromProtocolOp(op, result) {
|
|
|
286
309
|
return 'task-delegated';
|
|
287
310
|
}
|
|
288
311
|
if (op === 'task-update') {
|
|
289
|
-
|
|
290
|
-
|
|
312
|
+
// Same resolution as `isStateChangingProtocolEvent` above — `state` only.
|
|
313
|
+
const state = task?.state || r.state;
|
|
314
|
+
if (state === 'completed' || state === 'failed')
|
|
291
315
|
return 'task-closed';
|
|
292
316
|
return 'tool-result';
|
|
293
317
|
}
|
|
@@ -462,10 +486,9 @@ export function applyOutcomeToCtx(ctx, outcome) {
|
|
|
462
486
|
}
|
|
463
487
|
case 'task-closed': {
|
|
464
488
|
// Clear only this task's entry — sibling open tasks keep their books (ZIG-977).
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
ctx.activeTaskId;
|
|
489
|
+
// ZIG-1171: buildOutcomeOfKind already collapses result.task.taskId /
|
|
490
|
+
// result.taskId into outcome.taskId — do not re-walk the raw result here.
|
|
491
|
+
const closedId = outcome.taskId || ctx.activeTaskId;
|
|
469
492
|
if (closedId)
|
|
470
493
|
removeTaskBook(ctx, closedId);
|
|
471
494
|
else {
|
|
@@ -49,9 +49,10 @@ export function buildTaskIntakeState(initial) {
|
|
|
49
49
|
// Function form so a failed task_set_result surfaces as an error outcome
|
|
50
50
|
// (→ dflt error transition) instead of a phantom task-closed.
|
|
51
51
|
produces: (result) => {
|
|
52
|
+
// `state` only — ZIG-1057 cut `status` from the wire (ZIG-1128).
|
|
52
53
|
const r = result;
|
|
53
|
-
const
|
|
54
|
-
return
|
|
54
|
+
const state = r?.task?.state ?? r?.state;
|
|
55
|
+
return state === 'completed' || state === 'failed' ? 'task-closed' : 'error';
|
|
55
56
|
},
|
|
56
57
|
terminatesLoop: true,
|
|
57
58
|
},
|
|
@@ -63,9 +64,10 @@ export function buildTaskIntakeState(initial) {
|
|
|
63
64
|
},
|
|
64
65
|
tool: 'task_set_result',
|
|
65
66
|
produces: (result) => {
|
|
67
|
+
// `state` only — ZIG-1057 cut `status` from the wire (ZIG-1128).
|
|
66
68
|
const r = result;
|
|
67
|
-
const
|
|
68
|
-
return
|
|
69
|
+
const state = r?.task?.state ?? r?.state;
|
|
70
|
+
return state === 'completed' || state === 'failed' ? 'task-closed' : 'error';
|
|
69
71
|
},
|
|
70
72
|
terminatesLoop: true,
|
|
71
73
|
},
|
|
@@ -97,6 +97,13 @@ export function createWakeWriteKeys(laneKey, event) {
|
|
|
97
97
|
const identity = eventIdentity(event);
|
|
98
98
|
const wakeHash = identity === null ? null : sha(`${laneKey}\n${identity}`).slice(0, 40);
|
|
99
99
|
let ordinal = 0;
|
|
100
|
+
// ZIG-1171: one content-hash builder; tags differ, the shape does not.
|
|
101
|
+
const keyed = (tag, material, opts) => {
|
|
102
|
+
if (wakeHash === null)
|
|
103
|
+
return undefined;
|
|
104
|
+
const body = `wk_${wakeHash}_${tag}_${sha(material).slice(0, 16)}`;
|
|
105
|
+
return opts?.asMessageId ? `sdk_idem_${body}` : body;
|
|
106
|
+
};
|
|
100
107
|
return {
|
|
101
108
|
wakeKey: wakeHash === null ? null : `wk_${wakeHash}`,
|
|
102
109
|
artifactKey(content) {
|
|
@@ -110,37 +117,23 @@ export function createWakeWriteKeys(laneKey, event) {
|
|
|
110
117
|
// agent like agent-definer. Callers without content keep the ordinal.
|
|
111
118
|
if (content === undefined)
|
|
112
119
|
return `wk_${wakeHash}_a${ordinal++}`;
|
|
113
|
-
const
|
|
114
|
-
return
|
|
120
|
+
const material = typeof content === 'string' ? content : JSON.stringify(content) ?? '';
|
|
121
|
+
return keyed('a', material);
|
|
115
122
|
},
|
|
116
123
|
taskStateKey(taskId, state) {
|
|
117
|
-
|
|
118
|
-
return undefined;
|
|
119
|
-
return `wk_${wakeHash}_t_${sha(`${taskId}\n${state}`).slice(0, 16)}`;
|
|
124
|
+
return keyed('t', `${taskId}\n${state}`);
|
|
120
125
|
},
|
|
121
126
|
messageKey(fields) {
|
|
122
|
-
|
|
123
|
-
return undefined;
|
|
124
|
-
const msgHash = sha(`${fields.sessionId ?? ''}\n${fields.receiverId ?? ''}\n${fields.text}`).slice(0, 16);
|
|
125
|
-
return `sdk_idem_wk_${wakeHash}_m_${msgHash}`;
|
|
127
|
+
return keyed('m', `${fields.sessionId ?? ''}\n${fields.receiverId ?? ''}\n${fields.text}`, { asMessageId: true });
|
|
126
128
|
},
|
|
127
129
|
taskSpawnKey(fields) {
|
|
128
|
-
|
|
129
|
-
return undefined;
|
|
130
|
-
const spawnHash = sha(`${fields.agreementId}\n${fields.description}\n${fields.assigneeId ?? ''}\n${fields.parentTaskId ?? ''}`).slice(0, 16);
|
|
131
|
-
return `wk_${wakeHash}_s_${spawnHash}`;
|
|
130
|
+
return keyed('s', `${fields.agreementId}\n${fields.description}\n${fields.assigneeId ?? ''}\n${fields.parentTaskId ?? ''}`);
|
|
132
131
|
},
|
|
133
132
|
agreementProposeKey(fields) {
|
|
134
|
-
|
|
135
|
-
return undefined;
|
|
136
|
-
const proposeHash = sha(`${fields.proposedTo}\n${fields.providerId ?? ''}\n${fields.description}\n${fields.parentAgreementId ?? ''}`).slice(0, 16);
|
|
137
|
-
return `wk_${wakeHash}_ap_${proposeHash}`;
|
|
133
|
+
return keyed('ap', `${fields.proposedTo}\n${fields.providerId ?? ''}\n${fields.description}\n${fields.parentAgreementId ?? ''}`);
|
|
138
134
|
},
|
|
139
135
|
agreementSubcontractKey(fields) {
|
|
140
|
-
|
|
141
|
-
return undefined;
|
|
142
|
-
const subHash = sha(`${fields.executorId}\n${fields.description}\n${fields.parentAgreementId}`).slice(0, 16);
|
|
143
|
-
return `wk_${wakeHash}_asc_${subHash}`;
|
|
136
|
+
return keyed('asc', `${fields.executorId}\n${fields.description}\n${fields.parentAgreementId}`);
|
|
144
137
|
},
|
|
145
138
|
};
|
|
146
139
|
}
|