@ziggs-ai/agent-sdk 0.10.1 → 0.10.2
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/README.md +9 -1
- package/dist/agent/Agent.js +2 -2
- package/dist/agent/ClaudeSdkAgent.js +12 -10
- package/dist/agent/claudeCognition.d.ts +25 -3
- package/dist/agent/claudeCognition.js +33 -1
- package/dist/agent/claudeSdkTools.d.ts +2 -2
- package/dist/agent/claudeSdkTools.js +1 -1
- package/dist/agent/toolManagerToSdkMcp.js +1 -1
- package/dist/index.d.ts +4 -3
- package/dist/index.js +5 -2
- package/dist/ingress/normalizeIncoming.d.ts +2 -2
- package/dist/ingress/normalizeIncoming.js +1 -1
- package/dist/runtime/AgentMachine.d.ts +4 -4
- package/dist/runtime/AgentMachine.js +6 -6
- package/dist/runtime/{wakeWriteKeys.d.ts → deliveryWriteKeys.d.ts} +2 -2
- package/dist/runtime/{wakeWriteKeys.js → deliveryWriteKeys.js} +1 -1
- package/dist/runtime/runMechanical.d.ts +2 -2
- package/dist/runtime/runMechanical.js +2 -2
- package/dist/runtime/runTurn.d.ts +2 -2
- package/dist/runtime/runTurn.js +5 -5
- package/dist/server/ConnectionPool.d.ts +1 -1
- package/dist/server/ConnectionPool.js +3 -3
- package/dist/server/ZiggsEffectHandler.js +4 -4
- package/dist/server/createHealthServer.js +1 -1
- package/dist/server/tools/tier1/protocolRunner.d.ts +2 -2
- package/dist/server/tools/tier1/protocolRunner.js +9 -9
- package/dist/server/tools/tier1/protocolTools.js +1 -1
- package/dist/types.d.ts +4 -4
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -93,6 +93,14 @@ The public **`Brain`** interface is `tick(TickInput): Promise<TickOutput>`. Buil
|
|
|
93
93
|
|
|
94
94
|
Examples in the repo: `examples/agents/*.js` (e.g. coffee, expense, delivery agents).
|
|
95
95
|
|
|
96
|
+
Point the shared HTTP client at a non-default backend (self-hosted / local) before constructing hosts — no direct `@ziggs-ai/api-client` dependency needed:
|
|
97
|
+
|
|
98
|
+
```ts
|
|
99
|
+
import { configureApiClient, createAgent } from '@ziggs-ai/agent-sdk';
|
|
100
|
+
|
|
101
|
+
configureApiClient({ httpUrl: 'https://api.example.com' });
|
|
102
|
+
```
|
|
103
|
+
|
|
96
104
|
---
|
|
97
105
|
|
|
98
106
|
## Main exports (entry: `src/index.js`)
|
|
@@ -105,7 +113,7 @@ Examples in the repo: `examples/agents/*.js` (e.g. coffee, expense, delivery age
|
|
|
105
113
|
- **Tier 1 — protocol grammar (`server/tools/tier1/`, `PROTOCOL_TOOLS`).** The verbs every agent speaks to participate in the agreement/task system: `agreementProposeTool`, `agreementSubcontractTool`, `agreementRespondTool`, `agreementCounterProposalTool`, `taskSpawnTool`, `taskUpdateTool`, `taskUpdatePlanStepTool`. **Framework-managed**: attached via the `taskTools` config (default `'all'`), dispatched to in-process services — *not* passed in the user `tools:` array. Publishing open work is part of this grammar: `agreement_propose({ proposedTo: "everyone" })` (there is no separate publish tool).
|
|
106
114
|
- **Tier 2 — capability bundles (`server/tools/tier2/`, opt-in, HTTP-backed).** Discrete capability domains an agent chooses to have. **Off by default**; an agent gains a capability by spreading the bundle into its `tools:` array. Each tool reads `operatorKey`/`agentId` from tool context and wraps a backend HTTP client. Bundles: `DISCOVERY_TOOLS` (`agentSearchTool`, `agentGetTool`), `MARKETPLACE_TOOLS` (`marketplaceViewTool` — read-only), `PAYMENT_TOOLS` (`paymentBalanceTool`, `paymentTransferTool`, …).
|
|
107
115
|
- **Adapters / utils:** `OpenAIAdapter`, JSON helpers, formatters
|
|
108
|
-
- **Re-exported** from `@ziggs-ai/api-client`: `ContextReadClient`, `ContextDiscoveryClient`, `ContextGrantsClient`, `ArtifactsClient`, `MessagesClient`, `ScopeClient`, `AgentSearchClient`, `getBackendUrl`, `getWebSocketUrl`, etc. (There is no `ContextReader` / `ContextWriter` — use the clients above from `@ziggs-ai/api-client` directly. The WebSocket client was removed — delivery is HTTP inbox long-poll; read/ack via `InboxClient` from `@ziggs-ai/api-client`.)
|
|
116
|
+
- **Re-exported** from `@ziggs-ai/api-client`: `configureApiClient`, type `ApiClientConfig`, `ContextReadClient`, `ContextDiscoveryClient`, `ContextGrantsClient`, `ArtifactsClient`, `MessagesClient`, `ScopeClient`, `AgentSearchClient`, `getBackendUrl`, `getWebSocketUrl`, etc. (There is no `ContextReader` / `ContextWriter` — use the clients above from `@ziggs-ai/api-client` directly. The WebSocket client was removed — delivery is HTTP inbox long-poll; read/ack via `InboxClient` from `@ziggs-ai/api-client`.)
|
|
109
117
|
|
|
110
118
|
Package export: `"."` → `dist/index.js` (see `package.json`; `npm run build` before publish or monorepo tests).
|
|
111
119
|
|
package/dist/agent/Agent.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { AgentMachine } from '../runtime/AgentMachine.js';
|
|
2
|
-
import {
|
|
2
|
+
import { createDeliveryWriteKeys } from '../runtime/deliveryWriteKeys.js';
|
|
3
3
|
import { runTurn } from '../runtime/runTurn.js';
|
|
4
4
|
import { PromptBuilder } from '../runtime/PromptBuilder.js';
|
|
5
5
|
/**
|
|
@@ -46,7 +46,7 @@ export class Agent {
|
|
|
46
46
|
transitionTap,
|
|
47
47
|
// One key stream per wake, shared by every state this tick runs through —
|
|
48
48
|
// per-state streams would restart the ordinal and collide across states.
|
|
49
|
-
|
|
49
|
+
writeKeys: createDeliveryWriteKeys(identity.laneKey, event),
|
|
50
50
|
});
|
|
51
51
|
// `send` awaits the recursive _step until the machine settles at a
|
|
52
52
|
// parked state (or stops). When it returns, we have the final state.
|
|
@@ -2,7 +2,7 @@ import { query } from '@anthropic-ai/claude-agent-sdk';
|
|
|
2
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
|
-
import {
|
|
5
|
+
import { createDeliveryWriteKeys } from '../runtime/deliveryWriteKeys.js';
|
|
6
6
|
import { CLAUDE_SDK_PROTOCOL } from './claudeSdkProtocol.js';
|
|
7
7
|
import { buildZiggsSdkMcpServer } from './toolManagerToSdkMcp.js';
|
|
8
8
|
import { claudeQueryOptions, buildClaudeEventPrompt, summarizeContext, extractAssistantText, } from './claudeCognition.js';
|
|
@@ -222,11 +222,11 @@ export class ClaudeSdkAgent {
|
|
|
222
222
|
const nextCtx = bootstrapCtx(identity, ctx);
|
|
223
223
|
nextCtx.lastOutcome = outcomeFromEvent(rawEvent ?? null, identity.agentId);
|
|
224
224
|
const now = Date.now();
|
|
225
|
-
const
|
|
226
|
-
if (
|
|
227
|
-
runtimeLog.warn('ClaudeSdkAgent', `wake-rate breaker tripped: ${
|
|
225
|
+
const recentDeliveries = (nextCtx.recentDeliveries ?? []).filter((t) => now - t < WAKE_WINDOW_MS);
|
|
226
|
+
if (recentDeliveries.length >= MAX_WAKES_PER_WINDOW) {
|
|
227
|
+
runtimeLog.warn('ClaudeSdkAgent', `wake-rate breaker tripped: ${recentDeliveries.length} wakes in ${WAKE_WINDOW_MS / 60_000}min ` +
|
|
228
228
|
`session=${identity.sessionId} agent=${this.agentId} — skipping LLM for this wake`);
|
|
229
|
-
nextCtx.
|
|
229
|
+
nextCtx.recentDeliveries = recentDeliveries;
|
|
230
230
|
return {
|
|
231
231
|
ctx: nextCtx,
|
|
232
232
|
state: state ?? DEFAULT_STATE,
|
|
@@ -235,7 +235,7 @@ export class ClaudeSdkAgent {
|
|
|
235
235
|
metrics: { llmCalls: 0, tokens: { total: 0, input: 0, output: 0 } },
|
|
236
236
|
};
|
|
237
237
|
}
|
|
238
|
-
nextCtx.
|
|
238
|
+
nextCtx.recentDeliveries = [...recentDeliveries, now];
|
|
239
239
|
if (isNoticeOnlyWake(rawEvent, identity.agentId)) {
|
|
240
240
|
runtimeLog.info('ClaudeSdkAgent', `notice-only wake — skipping LLM session=${identity.sessionId} agent=${this.agentId} ` +
|
|
241
241
|
`type=${rawEvent?.type ?? 'unknown'}`);
|
|
@@ -296,12 +296,14 @@ export class ClaudeSdkAgent {
|
|
|
296
296
|
this.rolePrompt,
|
|
297
297
|
this.specialization ? `Specialization: ${this.specialization}` : '',
|
|
298
298
|
].filter(Boolean).join('\n');
|
|
299
|
-
|
|
299
|
+
// Two blocks, not one string: the shared protocol first so its cache prefix
|
|
300
|
+
// is reachable by every hosted agent, this agent's role second (ZIG-788).
|
|
301
|
+
const systemPrompt = [CLAUDE_SDK_PROTOCOL, roleBlock];
|
|
300
302
|
const runId = crypto.randomUUID();
|
|
301
303
|
// Replay-stable write keys for this wake (runId is random and can't serve):
|
|
302
304
|
// a crash-replay of the same event derives the same keys, so the backend
|
|
303
305
|
// drops the duplicate artifacts / no-ops the repeated task transitions.
|
|
304
|
-
const
|
|
306
|
+
const writeKeys = createDeliveryWriteKeys(identity.laneKey, rawEvent);
|
|
305
307
|
const toolContext = {
|
|
306
308
|
operatorKey: this.operatorKey,
|
|
307
309
|
agentId: this.agentId,
|
|
@@ -316,7 +318,7 @@ export class ClaudeSdkAgent {
|
|
|
316
318
|
// not just task_result events — see resolveOnBehalfAgreementId.
|
|
317
319
|
onBehalfOfAgreementId: resolveOnBehalfAgreementId(rawEvent, contextSnap, this.agentId, identity.sessionId),
|
|
318
320
|
runId,
|
|
319
|
-
|
|
321
|
+
writeKeys,
|
|
320
322
|
};
|
|
321
323
|
this.toolContextHolder.current = toolContext;
|
|
322
324
|
if (!this.sdkMcpServer) {
|
|
@@ -394,7 +396,7 @@ export class ClaudeSdkAgent {
|
|
|
394
396
|
if (text) {
|
|
395
397
|
// Ordinal drawn synchronously so the fire-and-forget below can't
|
|
396
398
|
// reorder key assignment against other writes of this wake.
|
|
397
|
-
const idempotencyKey =
|
|
399
|
+
const idempotencyKey = writeKeys.artifactKey({ kind: 'thought', text });
|
|
398
400
|
void eff({ kind: 'record-event', sessionId: identity.sessionId, entry: { kind: 'thought', text }, idempotencyKey })
|
|
399
401
|
.catch((err) => runtimeLog.warn('ClaudeSdkAgent', `record thought failed session=${identity.sessionId}: ${err.message}`));
|
|
400
402
|
}
|
|
@@ -11,11 +11,15 @@
|
|
|
11
11
|
* it MUST be applied on every Claude-brain wake. Centralizing it here means a
|
|
12
12
|
* caller cannot silently drift from it.
|
|
13
13
|
*/
|
|
14
|
-
import type
|
|
14
|
+
import { type Options, type SDKAssistantMessage } from '@anthropic-ai/claude-agent-sdk';
|
|
15
15
|
import type { RawEvent } from '../runtime/buildOutcome.js';
|
|
16
16
|
export interface ClaudeQueryOptionsParams {
|
|
17
|
-
/**
|
|
18
|
-
|
|
17
|
+
/**
|
|
18
|
+
* The system prompt in cache-ordered blocks: the shared protocol first, then
|
|
19
|
+
* this agent's role. Both are stable across wakes — see
|
|
20
|
+
* {@link claudeQueryOptions} for why that matters.
|
|
21
|
+
*/
|
|
22
|
+
systemPrompt: string[];
|
|
19
23
|
/** Claude model id for the brain. */
|
|
20
24
|
model: string;
|
|
21
25
|
/** In-process MCP servers exposing the agent's tools (e.g. `{ ziggs }`). */
|
|
@@ -47,6 +51,24 @@ export declare function resolveClaudeModel(preferred?: string, servicesModel?: s
|
|
|
47
51
|
* Build the `query()` options for a Claude-brain wake. The lockdown fields are
|
|
48
52
|
* fixed here; callers only supply the per-wake variables (prompt, model, tools,
|
|
49
53
|
* turn budget, resume). Returns `Options` so it drops straight into `query()`.
|
|
54
|
+
*
|
|
55
|
+
* ZIG-788 — prompt caching. The system prompt is handed over as blocks with a
|
|
56
|
+
* trailing `SYSTEM_PROMPT_DYNAMIC_BOUNDARY`: everything before the marker is
|
|
57
|
+
* eligible for cross-session caching, and there is nothing after it because
|
|
58
|
+
* nothing in this prompt varies per wake. Every per-wake input — the event, the
|
|
59
|
+
* context digest — is in the user prompt (`buildClaudeEventPrompt`), which sits
|
|
60
|
+
* after the cached prefix and cannot invalidate it.
|
|
61
|
+
*
|
|
62
|
+
* Cross-SESSION is the point, not cross-turn: `statelessPerWake` starts a fresh
|
|
63
|
+
* transcript on every wake, so without this the identical prefix was re-read at
|
|
64
|
+
* full price on each one.
|
|
65
|
+
*
|
|
66
|
+
* Caching is a prefix match, so block ORDER decides how far the hit reaches:
|
|
67
|
+
* the protocol block is byte-identical for every hosted agent and goes first,
|
|
68
|
+
* the role block is per-agent and goes second. An agent whose combined prompt
|
|
69
|
+
* lands under the model's minimum cacheable prefix (~1k tokens on Sonnet, more
|
|
70
|
+
* on Haiku) simply writes no entry — that is a silent no-op, not an error, so
|
|
71
|
+
* read `cacheRead`/`cacheCreation` in the wake metrics rather than assuming.
|
|
50
72
|
*/
|
|
51
73
|
export declare function claudeQueryOptions(params: ClaudeQueryOptionsParams): Options;
|
|
52
74
|
/**
|
|
@@ -1,3 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared Claude-Agent-SDK cognition pieces.
|
|
3
|
+
*
|
|
4
|
+
* Everything a Claude-brain agent needs to turn a Ziggs wake into a `query()`
|
|
5
|
+
* call, EXCEPT the loop itself. `ClaudeSdkAgent` owns the loop (drive `query()`,
|
|
6
|
+
* collect metrics); this module owns the periphery so it lives in one place and
|
|
7
|
+
* is unit-testable without spinning up the SDK.
|
|
8
|
+
*
|
|
9
|
+
* The lockdown option set (`tools: []`, `strictMcpConfig`, `settingSources: []`,
|
|
10
|
+
* bypass permissions, no local settings) is a security boundary, not a default —
|
|
11
|
+
* it MUST be applied on every Claude-brain wake. Centralizing it here means a
|
|
12
|
+
* caller cannot silently drift from it.
|
|
13
|
+
*/
|
|
14
|
+
import { SYSTEM_PROMPT_DYNAMIC_BOUNDARY, } from '@anthropic-ai/claude-agent-sdk';
|
|
1
15
|
import { getBatchEvents } from '../context/batch.js';
|
|
2
16
|
const DEFAULT_CLAUDE_MODEL = 'claude-sonnet-4-6';
|
|
3
17
|
/**
|
|
@@ -17,10 +31,28 @@ export function resolveClaudeModel(preferred, servicesModel) {
|
|
|
17
31
|
* Build the `query()` options for a Claude-brain wake. The lockdown fields are
|
|
18
32
|
* fixed here; callers only supply the per-wake variables (prompt, model, tools,
|
|
19
33
|
* turn budget, resume). Returns `Options` so it drops straight into `query()`.
|
|
34
|
+
*
|
|
35
|
+
* ZIG-788 — prompt caching. The system prompt is handed over as blocks with a
|
|
36
|
+
* trailing `SYSTEM_PROMPT_DYNAMIC_BOUNDARY`: everything before the marker is
|
|
37
|
+
* eligible for cross-session caching, and there is nothing after it because
|
|
38
|
+
* nothing in this prompt varies per wake. Every per-wake input — the event, the
|
|
39
|
+
* context digest — is in the user prompt (`buildClaudeEventPrompt`), which sits
|
|
40
|
+
* after the cached prefix and cannot invalidate it.
|
|
41
|
+
*
|
|
42
|
+
* Cross-SESSION is the point, not cross-turn: `statelessPerWake` starts a fresh
|
|
43
|
+
* transcript on every wake, so without this the identical prefix was re-read at
|
|
44
|
+
* full price on each one.
|
|
45
|
+
*
|
|
46
|
+
* Caching is a prefix match, so block ORDER decides how far the hit reaches:
|
|
47
|
+
* the protocol block is byte-identical for every hosted agent and goes first,
|
|
48
|
+
* the role block is per-agent and goes second. An agent whose combined prompt
|
|
49
|
+
* lands under the model's minimum cacheable prefix (~1k tokens on Sonnet, more
|
|
50
|
+
* on Haiku) simply writes no entry — that is a silent no-op, not an error, so
|
|
51
|
+
* read `cacheRead`/`cacheCreation` in the wake metrics rather than assuming.
|
|
20
52
|
*/
|
|
21
53
|
export function claudeQueryOptions(params) {
|
|
22
54
|
return {
|
|
23
|
-
systemPrompt: params.systemPrompt,
|
|
55
|
+
systemPrompt: [...params.systemPrompt, SYSTEM_PROMPT_DYNAMIC_BOUNDARY],
|
|
24
56
|
model: params.model,
|
|
25
57
|
mcpServers: params.mcpServers,
|
|
26
58
|
// The agent's ONLY capabilities are its MCP tools — no built-in file/bash/web
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { type ToolDefinition } from '../tools/defineTool.js';
|
|
2
|
-
import type { EffectHandler,
|
|
2
|
+
import type { EffectHandler, DeliveryWriteKeys } from '../types.js';
|
|
3
3
|
export interface ClaudeSdkToolContext extends Record<string, unknown> {
|
|
4
4
|
operatorKey: string;
|
|
5
5
|
agentId: string;
|
|
@@ -14,7 +14,7 @@ export interface ClaudeSdkToolContext extends Record<string, unknown> {
|
|
|
14
14
|
/** Per-wake id grouping this tick's tool calls in telemetry (ZIG-775). Random — never use for idempotency. */
|
|
15
15
|
runId?: string;
|
|
16
16
|
/** Replay-stable write keys for this wake — write tools derive idempotency keys from here, never from the LLM. */
|
|
17
|
-
|
|
17
|
+
writeKeys?: DeliveryWriteKeys;
|
|
18
18
|
}
|
|
19
19
|
export declare const contextSnapshotTool: ToolDefinition;
|
|
20
20
|
export declare const chatSendTool: ToolDefinition;
|
|
@@ -77,7 +77,7 @@ export const chatSendTool = defineTool('chat_send', {
|
|
|
77
77
|
const text = String(args.text);
|
|
78
78
|
const messageId = args.idempotencyKey
|
|
79
79
|
? `sdk_idem_${String(args.idempotencyKey)}`
|
|
80
|
-
: c.
|
|
80
|
+
: c.writeKeys?.messageKey({ text, sessionId, receiverId }) ?? `sdk_${randomUUID()}`;
|
|
81
81
|
return c.eff({
|
|
82
82
|
kind: 'send-message',
|
|
83
83
|
sessionId,
|
|
@@ -100,7 +100,7 @@ export function buildSdkToolDefs(toolDefs, getContext) {
|
|
|
100
100
|
senderType: ctx.senderType,
|
|
101
101
|
onBehalfOfAgreementId: ctx.onBehalfOfAgreementId,
|
|
102
102
|
runId: ctx.runId,
|
|
103
|
-
|
|
103
|
+
writeKeys: ctx.writeKeys,
|
|
104
104
|
});
|
|
105
105
|
if (r.ok)
|
|
106
106
|
return toCallToolResult(r.result);
|
package/dist/index.d.ts
CHANGED
|
@@ -35,8 +35,8 @@ export { validateWorkflow, WorkflowValidationError } from './runtime/validateWor
|
|
|
35
35
|
export { deriveRespondsToProposals, statesRespondToProposals, } from './runtime/deriveRespondsToProposals.js';
|
|
36
36
|
export { outcomeFromEvent, outcomeFromActionResult, applyOutcomeToCtx } from './runtime/buildOutcome.js';
|
|
37
37
|
export { LANE_BOOK_KEY, allCollectedResults, allDelegatedAgreementIds, allPendingSubtasks, } from './runtime/taskBookkeeping.js';
|
|
38
|
-
export {
|
|
39
|
-
export type { Outcome, OutcomeKind, Ctx, Identity, HistoryEntry, ToolResultEntry, PendingSubtask, CollectedSubtaskResult, TaskBookkeeping, Workflow, State, ParkedState, ThinkingState, MechanicalState, Action, Transition, PromptDef, ActionPrompt, Produces, ProducesFn, AgreementRef, TaskRef, ProposalRef, AgentConfig, DefineAgentInput, ServicesConfig, Effect, EffectKind, EffectResult, EffectResultMap, EffectHandler, LlmMessage, LlmToolCall, LlmToolSchema, LlmResponse, ContextSnapshot, ReadContextOpts, ToolCallResult, RecordedEntry,
|
|
38
|
+
export { createDeliveryWriteKeys } from './runtime/deliveryWriteKeys.js';
|
|
39
|
+
export type { Outcome, OutcomeKind, Ctx, Identity, HistoryEntry, ToolResultEntry, PendingSubtask, CollectedSubtaskResult, TaskBookkeeping, Workflow, State, ParkedState, ThinkingState, MechanicalState, Action, Transition, PromptDef, ActionPrompt, Produces, ProducesFn, AgreementRef, TaskRef, ProposalRef, AgentConfig, DefineAgentInput, ServicesConfig, Effect, EffectKind, EffectResult, EffectResultMap, EffectHandler, LlmMessage, LlmToolCall, LlmToolSchema, LlmResponse, ContextSnapshot, ReadContextOpts, ToolCallResult, RecordedEntry, DeliveryWriteKeys, } from './types.js';
|
|
40
40
|
export { default } from './AgentHost.js';
|
|
41
41
|
export { validateContext, ContextWarning } from './cognition/validateContext.js';
|
|
42
42
|
export { runLauncher } from './server/runLauncher.js';
|
|
@@ -66,7 +66,8 @@ export { classifyIncomingEvent } from './context/classifyEnvelope.js';
|
|
|
66
66
|
export { buildContextUpdates, CONTEXT_RESET } from './context/applyEffects.js';
|
|
67
67
|
export { getBatchEvents, isTaskResultRelevantToAgent } from './context/batch.js';
|
|
68
68
|
export { classifyWorkflowEvent, findTaskResult, unwrapBatchEvent, findIncomingTaskResult } from './context/routingLabels.js';
|
|
69
|
-
export { MessagesClient, ArtifactsClient, AgentSearchClient, getBackendUrl, } from '@ziggs-ai/api-client';
|
|
69
|
+
export { MessagesClient, ArtifactsClient, AgentSearchClient, getBackendUrl, configureApiClient, } from '@ziggs-ai/api-client';
|
|
70
|
+
export type { ApiClientConfig } from '@ziggs-ai/api-client';
|
|
70
71
|
export { SeenMessages } from './server/SeenMessages.js';
|
|
71
72
|
export { runtimeLog, resetRuntimeLogLevelCache } from './shared/runtimeLog.js';
|
|
72
73
|
export type { RuntimeLogLevel } from './shared/runtimeLog.js';
|
package/dist/index.js
CHANGED
|
@@ -29,7 +29,7 @@ export { validateWorkflow, WorkflowValidationError } from './runtime/validateWor
|
|
|
29
29
|
export { deriveRespondsToProposals, statesRespondToProposals, } from './runtime/deriveRespondsToProposals.js';
|
|
30
30
|
export { outcomeFromEvent, outcomeFromActionResult, applyOutcomeToCtx } from './runtime/buildOutcome.js';
|
|
31
31
|
export { LANE_BOOK_KEY, allCollectedResults, allDelegatedAgreementIds, allPendingSubtasks, } from './runtime/taskBookkeeping.js';
|
|
32
|
-
export {
|
|
32
|
+
export { createDeliveryWriteKeys } from './runtime/deliveryWriteKeys.js';
|
|
33
33
|
export { default } from './AgentHost.js';
|
|
34
34
|
// ── Supporting modules ──────────────────────────────────────────────────────
|
|
35
35
|
export { validateContext, ContextWarning } from './cognition/validateContext.js';
|
|
@@ -71,6 +71,9 @@ export { classifyIncomingEvent } from './context/classifyEnvelope.js';
|
|
|
71
71
|
export { buildContextUpdates, CONTEXT_RESET } from './context/applyEffects.js';
|
|
72
72
|
export { getBatchEvents, isTaskResultRelevantToAgent } from './context/batch.js';
|
|
73
73
|
export { classifyWorkflowEvent, findTaskResult, unwrapBatchEvent, findIncomingTaskResult } from './context/routingLabels.js';
|
|
74
|
-
export { MessagesClient, ArtifactsClient, AgentSearchClient, getBackendUrl,
|
|
74
|
+
export { MessagesClient, ArtifactsClient, AgentSearchClient, getBackendUrl,
|
|
75
|
+
// ZIG-1204 — self-hosted / non-default backend: consumers of agent-sdk alone
|
|
76
|
+
// must be able to point the shared HTTP client without a direct api-client dep.
|
|
77
|
+
configureApiClient, } from '@ziggs-ai/api-client';
|
|
75
78
|
export { SeenMessages } from './server/SeenMessages.js';
|
|
76
79
|
export { runtimeLog, resetRuntimeLogLevelCache } from './shared/runtimeLog.js';
|
|
@@ -7,7 +7,7 @@ export interface NormalizedEvent {
|
|
|
7
7
|
/**
|
|
8
8
|
* Backend per-delivery id of the chat row this event came from. The one
|
|
9
9
|
* field of the event that is identical on live push and on catch-up
|
|
10
|
-
* redelivery, so it anchors the wake's idempotency keys (
|
|
10
|
+
* redelivery, so it anchors the wake's idempotency keys (deliveryWriteKeys).
|
|
11
11
|
*/
|
|
12
12
|
messageId?: string;
|
|
13
13
|
timestamp: number;
|
|
@@ -92,7 +92,7 @@ interface NormalizeArgs {
|
|
|
92
92
|
* what is fenced to this agent, so there is no client-side "is this for me"
|
|
93
93
|
* relevance gate — this function decides SHAPE (message vs task_result vs
|
|
94
94
|
* agreement_lifecycle vs notification) and the fields the FSM / SessionResolver /
|
|
95
|
-
*
|
|
95
|
+
* deliveryWriteKeys depend on, nothing more. `ownAgentId` is accepted for call-site
|
|
96
96
|
* compatibility but no longer gates anything.
|
|
97
97
|
*/
|
|
98
98
|
export declare function normalizeIncomingEvent({ text, metadata }: NormalizeArgs): NormalizedEvent;
|
|
@@ -86,7 +86,7 @@ function isTerminalTaskState(state) {
|
|
|
86
86
|
* what is fenced to this agent, so there is no client-side "is this for me"
|
|
87
87
|
* relevance gate — this function decides SHAPE (message vs task_result vs
|
|
88
88
|
* agreement_lifecycle vs notification) and the fields the FSM / SessionResolver /
|
|
89
|
-
*
|
|
89
|
+
* deliveryWriteKeys depend on, nothing more. `ownAgentId` is accepted for call-site
|
|
90
90
|
* compatibility but no longer gates anything.
|
|
91
91
|
*/
|
|
92
92
|
export function normalizeIncomingEvent({ text, metadata = {} }) {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { Ctx, Outcome, Workflow, Identity, EffectHandler,
|
|
1
|
+
import type { Ctx, Outcome, Workflow, Identity, EffectHandler, DeliveryWriteKeys } from '../types.js';
|
|
2
2
|
import { type RawEvent } from './buildOutcome.js';
|
|
3
3
|
import type { runTurn as RunTurnFn } from './runTurn.js';
|
|
4
4
|
import { PromptBuilder } from './PromptBuilder.js';
|
|
@@ -60,7 +60,7 @@ interface MachineOptions {
|
|
|
60
60
|
/** Runaway-loop backstop: max thinking turns without human input before parking. Default 100. */
|
|
61
61
|
maxTurnsWithoutUserInput?: number;
|
|
62
62
|
/** This wake's replay-stable write keys — one stream shared by every turn of the tick. */
|
|
63
|
-
|
|
63
|
+
writeKeys?: DeliveryWriteKeys;
|
|
64
64
|
}
|
|
65
65
|
interface IncomingFrame {
|
|
66
66
|
event?: RawEvent;
|
|
@@ -86,7 +86,7 @@ export declare class AgentMachine {
|
|
|
86
86
|
toolManager: ToolManager;
|
|
87
87
|
promptBuilder: PromptBuilder;
|
|
88
88
|
memory?: MemoryStore;
|
|
89
|
-
|
|
89
|
+
writeKeys?: DeliveryWriteKeys;
|
|
90
90
|
state: string;
|
|
91
91
|
status: 'active' | 'stopped';
|
|
92
92
|
ctx: Ctx;
|
|
@@ -114,7 +114,7 @@ export declare class AgentMachine {
|
|
|
114
114
|
output: number;
|
|
115
115
|
};
|
|
116
116
|
get stateTimeoutUnhandled(): boolean;
|
|
117
|
-
constructor({ workflow, executionCore, eff, toolManager, promptBuilder, memory, input, transitionTap, maxTurnsWithoutUserInput,
|
|
117
|
+
constructor({ workflow, executionCore, eff, toolManager, promptBuilder, memory, input, transitionTap, maxTurnsWithoutUserInput, writeKeys }: MachineOptions);
|
|
118
118
|
getSnapshot(): MachineSnapshot;
|
|
119
119
|
/** Parked = currently in a parked state and not executing a turn. */
|
|
120
120
|
get isParked(): boolean;
|
|
@@ -30,7 +30,7 @@ export class AgentMachine {
|
|
|
30
30
|
toolManager;
|
|
31
31
|
promptBuilder;
|
|
32
32
|
memory;
|
|
33
|
-
|
|
33
|
+
writeKeys;
|
|
34
34
|
state;
|
|
35
35
|
status = 'active';
|
|
36
36
|
ctx;
|
|
@@ -50,14 +50,14 @@ export class AgentMachine {
|
|
|
50
50
|
get llmCalls() { return this._llmCalls; }
|
|
51
51
|
get tokens() { return this._tokens; }
|
|
52
52
|
get stateTimeoutUnhandled() { return this._stateTimeoutUnhandled; }
|
|
53
|
-
constructor({ workflow, executionCore, eff, toolManager, promptBuilder, memory, input, transitionTap, maxTurnsWithoutUserInput,
|
|
53
|
+
constructor({ workflow, executionCore, eff, toolManager, promptBuilder, memory, input, transitionTap, maxTurnsWithoutUserInput, writeKeys }) {
|
|
54
54
|
this.workflow = workflow;
|
|
55
55
|
this.executionCore = executionCore;
|
|
56
56
|
this.eff = eff;
|
|
57
57
|
this.toolManager = toolManager;
|
|
58
58
|
this.promptBuilder = promptBuilder;
|
|
59
59
|
this.memory = memory;
|
|
60
|
-
this.
|
|
60
|
+
this.writeKeys = writeKeys;
|
|
61
61
|
this._transitionTap = transitionTap;
|
|
62
62
|
this._maxTurnsWithoutUserInput = maxTurnsWithoutUserInput ?? DEFAULT_MAX_TURNS_WITHOUT_USER_INPUT;
|
|
63
63
|
this.state = input.state || workflow.initial;
|
|
@@ -202,7 +202,7 @@ export class AgentMachine {
|
|
|
202
202
|
eff: this.eff,
|
|
203
203
|
memory: this.memory,
|
|
204
204
|
incomingEvent: this._lastIncomingEvent,
|
|
205
|
-
|
|
205
|
+
writeKeys: this.writeKeys,
|
|
206
206
|
});
|
|
207
207
|
this.ctx.toolResults = toolResults;
|
|
208
208
|
this._llmCalls += llmCalls;
|
|
@@ -296,7 +296,7 @@ export class AgentMachine {
|
|
|
296
296
|
promptBuilder: this.promptBuilder,
|
|
297
297
|
memory: this.memory,
|
|
298
298
|
definition: this.workflow,
|
|
299
|
-
|
|
299
|
+
writeKeys: this.writeKeys,
|
|
300
300
|
});
|
|
301
301
|
this.ctx.toolResults = toolResults;
|
|
302
302
|
this._llmCalls += llmCalls;
|
|
@@ -365,7 +365,7 @@ export class AgentMachine {
|
|
|
365
365
|
sessionId,
|
|
366
366
|
receiverId: userId,
|
|
367
367
|
text,
|
|
368
|
-
messageId: this.
|
|
368
|
+
messageId: this.writeKeys?.messageKey({ text, sessionId, receiverId: userId }),
|
|
369
369
|
});
|
|
370
370
|
}
|
|
371
371
|
catch { /* best-effort */ }
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { DeliveryWriteKeys } from '../types.js';
|
|
2
2
|
/**
|
|
3
3
|
* Build the per-wake key derivation. Call once per tick with the lane and the
|
|
4
4
|
* raw triggering event, then thread the instance to every write of the wake.
|
|
@@ -24,4 +24,4 @@ import type { WakeWriteKeys } from '../types.js';
|
|
|
24
24
|
* as task spawns; the backend dedups on (creator, key) so a replayed
|
|
25
25
|
* propose/subcontract resolves to the original agreement.
|
|
26
26
|
*/
|
|
27
|
-
export declare function
|
|
27
|
+
export declare function createDeliveryWriteKeys(laneKey: string, event: unknown): DeliveryWriteKeys;
|
|
@@ -93,7 +93,7 @@ function sha(input) {
|
|
|
93
93
|
* as task spawns; the backend dedups on (creator, key) so a replayed
|
|
94
94
|
* propose/subcontract resolves to the original agreement.
|
|
95
95
|
*/
|
|
96
|
-
export function
|
|
96
|
+
export function createDeliveryWriteKeys(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;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { Ctx, EffectHandler, MechanicalState, Outcome, ToolResultEntry,
|
|
1
|
+
import type { Ctx, EffectHandler, MechanicalState, Outcome, ToolResultEntry, DeliveryWriteKeys } from '../types.js';
|
|
2
2
|
import type { MemoryStore } from '../memory/MemoryStore.js';
|
|
3
3
|
import { type RawEvent } from './buildOutcome.js';
|
|
4
4
|
export interface RunMechanicalResult {
|
|
@@ -23,5 +23,5 @@ export declare function runMechanical(input: {
|
|
|
23
23
|
memory?: MemoryStore;
|
|
24
24
|
incomingEvent: RawEvent | null;
|
|
25
25
|
/** This wake's replay-stable write keys (shared across every turn of the tick — see AgentMachine). */
|
|
26
|
-
|
|
26
|
+
writeKeys?: DeliveryWriteKeys;
|
|
27
27
|
}): Promise<RunMechanicalResult>;
|
|
@@ -5,12 +5,12 @@ import { runtimeLog } from '../shared/runtimeLog.js';
|
|
|
5
5
|
* Used by dumb coordinators whose only job is a fixed tool call on task-assigned.
|
|
6
6
|
*/
|
|
7
7
|
export async function runMechanical(input) {
|
|
8
|
-
const { stateId, state, ctx, eff, memory, incomingEvent,
|
|
8
|
+
const { stateId, state, ctx, eff, memory, incomingEvent, writeKeys } = input;
|
|
9
9
|
const sessionId = ctx.identity.sessionId;
|
|
10
10
|
const runId = crypto.randomUUID();
|
|
11
11
|
const effWithTrace = (effect) => {
|
|
12
12
|
if (effect.kind === 'tool-call') {
|
|
13
|
-
return eff({ ...effect, runId, stateId,
|
|
13
|
+
return eff({ ...effect, runId, stateId, writeKeys });
|
|
14
14
|
}
|
|
15
15
|
return eff(effect);
|
|
16
16
|
};
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { Action, Ctx, Outcome, ToolResultEntry, EffectHandler,
|
|
1
|
+
import type { Action, Ctx, Outcome, ToolResultEntry, EffectHandler, DeliveryWriteKeys } from '../types.js';
|
|
2
2
|
import type { MemoryStore } from '../memory/MemoryStore.js';
|
|
3
3
|
import { type RawEvent } from './buildOutcome.js';
|
|
4
4
|
import { PromptBuilder } from './PromptBuilder.js';
|
|
@@ -33,7 +33,7 @@ interface RunTurnInput {
|
|
|
33
33
|
*/
|
|
34
34
|
streamRespondActions?: boolean;
|
|
35
35
|
/** This wake's replay-stable write keys (shared across every turn of the tick — see AgentMachine). */
|
|
36
|
-
|
|
36
|
+
writeKeys?: DeliveryWriteKeys;
|
|
37
37
|
}
|
|
38
38
|
interface RunTurnResult {
|
|
39
39
|
outcome: Outcome;
|
package/dist/runtime/runTurn.js
CHANGED
|
@@ -45,7 +45,7 @@ function isGenericFallbackTool(name, toolManager) {
|
|
|
45
45
|
* call and (optionally) wrapping it in telemetry / dedup / retries.
|
|
46
46
|
*/
|
|
47
47
|
export async function runTurn(input) {
|
|
48
|
-
const { stateId, statePrompt, actions, incomingEvent, ctx, eff, toolManager, promptBuilder, memory, definition,
|
|
48
|
+
const { stateId, statePrompt, actions, incomingEvent, ctx, eff, toolManager, promptBuilder, memory, definition, writeKeys } = input;
|
|
49
49
|
// Default on (ZIG-838); ENABLE_LLM_STREAMING=false is the deployment
|
|
50
50
|
// kill-switch. Any other value (unset, 'true', …) streams.
|
|
51
51
|
const streamRespondActions = input.streamRespondActions ?? process.env.ENABLE_LLM_STREAMING !== 'false';
|
|
@@ -62,13 +62,13 @@ export async function runTurn(input) {
|
|
|
62
62
|
return eff({ ...effect, runId, stateId });
|
|
63
63
|
}
|
|
64
64
|
if (effect.kind === 'tool-call') {
|
|
65
|
-
return eff({ ...effect, runId, stateId,
|
|
65
|
+
return eff({ ...effect, runId, stateId, writeKeys });
|
|
66
66
|
}
|
|
67
67
|
if (effect.kind === 'send-message' && !effect.messageId) {
|
|
68
68
|
const send = effect;
|
|
69
69
|
return eff({
|
|
70
70
|
...effect,
|
|
71
|
-
messageId:
|
|
71
|
+
messageId: writeKeys?.messageKey({
|
|
72
72
|
text: send.text,
|
|
73
73
|
sessionId: send.sessionId,
|
|
74
74
|
receiverId: send.receiverId,
|
|
@@ -84,7 +84,7 @@ export async function runTurn(input) {
|
|
|
84
84
|
if (s.length === 0)
|
|
85
85
|
return;
|
|
86
86
|
try {
|
|
87
|
-
await eff({ kind: 'record-event', sessionId, entry: { kind: 'thought', text: s }, idempotencyKey:
|
|
87
|
+
await eff({ kind: 'record-event', sessionId, entry: { kind: 'thought', text: s }, idempotencyKey: writeKeys?.artifactKey({ kind: 'thought', text: s }) });
|
|
88
88
|
}
|
|
89
89
|
catch (err) {
|
|
90
90
|
runtimeLog.warn('runTurn', `record-event:thought failed: ${err.message}`);
|
|
@@ -380,7 +380,7 @@ export async function runTurn(input) {
|
|
|
380
380
|
}
|
|
381
381
|
const safeText = truncateMsg(replyText);
|
|
382
382
|
persistMessageId =
|
|
383
|
-
|
|
383
|
+
writeKeys?.messageKey({
|
|
384
384
|
text: safeText,
|
|
385
385
|
sessionId,
|
|
386
386
|
receiverId: decision.receiverId,
|
|
@@ -27,7 +27,7 @@ export declare class ConnectionPool {
|
|
|
27
27
|
connectAll(): Promise<void>;
|
|
28
28
|
/** The host for one agent, connecting it if it somehow is not up yet. */
|
|
29
29
|
startAgent(agentId: string): Promise<AgentHost>;
|
|
30
|
-
|
|
30
|
+
stopAgent(agentId: string): Promise<void>;
|
|
31
31
|
disconnectAll(): Promise<void>;
|
|
32
32
|
/**
|
|
33
33
|
* Deliver a message to the named agent and wait for its first outbound
|
|
@@ -72,11 +72,11 @@ export class ConnectionPool {
|
|
|
72
72
|
async startAgent(agentId) {
|
|
73
73
|
return await this.manager.startAgent(agentId);
|
|
74
74
|
}
|
|
75
|
-
async
|
|
76
|
-
return this.manager.
|
|
75
|
+
async stopAgent(agentId) {
|
|
76
|
+
return this.manager.stopAgent(agentId);
|
|
77
77
|
}
|
|
78
78
|
async disconnectAll() {
|
|
79
|
-
return this.manager.
|
|
79
|
+
return this.manager.stopAll();
|
|
80
80
|
}
|
|
81
81
|
/**
|
|
82
82
|
* Deliver a message to the named agent and wait for its first outbound
|
|
@@ -116,7 +116,7 @@ export async function executeToolWithSideEffects(deps, eff, effect, roster) {
|
|
|
116
116
|
state: 'started',
|
|
117
117
|
tool: effect.name,
|
|
118
118
|
...(argsPreview ? { args: argsPreview } : {}),
|
|
119
|
-
}, effect.
|
|
119
|
+
}, effect.writeKeys?.artifactKey({ s: 'started', t: effect.name, a: argsPreview }));
|
|
120
120
|
try {
|
|
121
121
|
const lastUser = roster.users.find((u) => u.userId || u.id);
|
|
122
122
|
// Accumulate token usage from any nested LLM call the tool makes (llmTool),
|
|
@@ -152,7 +152,7 @@ export async function executeToolWithSideEffects(deps, eff, effect, roster) {
|
|
|
152
152
|
senderType: effect.senderType ?? (lastUser ? 'USER' : undefined),
|
|
153
153
|
onBehalfOfAgreementId: effect.onBehalfOfAgreementId,
|
|
154
154
|
memory: effect.memory,
|
|
155
|
-
|
|
155
|
+
writeKeys: effect.writeKeys,
|
|
156
156
|
reportUsage,
|
|
157
157
|
turnScratch: {},
|
|
158
158
|
});
|
|
@@ -163,7 +163,7 @@ export async function executeToolWithSideEffects(deps, eff, effect, roster) {
|
|
|
163
163
|
state: 'completed',
|
|
164
164
|
tool: effect.name,
|
|
165
165
|
result: clipped,
|
|
166
|
-
}, effect.
|
|
166
|
+
}, effect.writeKeys?.artifactKey({ s: 'completed', t: effect.name, r: clipped }));
|
|
167
167
|
sendTelemetry(deps.telemetryClient, buildToolIngestPayload({
|
|
168
168
|
sessionId: effect.sessionId,
|
|
169
169
|
tool: effect.name,
|
|
@@ -181,7 +181,7 @@ export async function executeToolWithSideEffects(deps, eff, effect, roster) {
|
|
|
181
181
|
state: 'failed',
|
|
182
182
|
tool: effect.name,
|
|
183
183
|
error: errMsg,
|
|
184
|
-
}, effect.
|
|
184
|
+
}, effect.writeKeys?.artifactKey({ s: 'failed', t: effect.name, e: errMsg }));
|
|
185
185
|
sendTelemetry(deps.telemetryClient, buildToolIngestPayload({
|
|
186
186
|
sessionId: effect.sessionId,
|
|
187
187
|
tool: effect.name,
|
|
@@ -19,7 +19,7 @@ function readJsonBody(req) {
|
|
|
19
19
|
export function createHealthServer({ port = Number(process.env.PORT) || 8080, label = 'agents', pool, } = {}) {
|
|
20
20
|
const server = createServer(async (req, res) => {
|
|
21
21
|
const url = req.url ?? '/';
|
|
22
|
-
const startMatch = url.match(/^\/fleet-
|
|
22
|
+
const startMatch = url.match(/^\/fleet-start\/([^/?]+)/);
|
|
23
23
|
if (req.method === 'POST' && startMatch && pool) {
|
|
24
24
|
const agentId = decodeURIComponent(startMatch[1]);
|
|
25
25
|
try {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { AgreementService } from '../../agreements/AgreementService.js';
|
|
2
2
|
import type { TaskService } from '../../tasks/TaskService.js';
|
|
3
|
-
import type {
|
|
3
|
+
import type { DeliveryWriteKeys } from '../../../types.js';
|
|
4
4
|
import { type ContextReadClient } from '@ziggs-ai/api-client';
|
|
5
5
|
interface Services {
|
|
6
6
|
agreementService: AgreementService;
|
|
@@ -36,7 +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,
|
|
39
|
+
export declare function dispatchProtocolOp(services: Services, payload: ProtocolPayload, chatId: string | null, agents?: unknown[], actingAgentId?: string | null, writeKeys?: DeliveryWriteKeys | null,
|
|
40
40
|
/** ZIG-1120 #8 — prefer the cached session snapshot over a fresh HTTP read. */
|
|
41
41
|
cachedAgreements?: unknown[]): Promise<unknown>;
|
|
42
42
|
export {};
|
|
@@ -45,7 +45,7 @@ function normalizePlanInPlace(payload) {
|
|
|
45
45
|
? 'before_execution'
|
|
46
46
|
: 'with_proposal';
|
|
47
47
|
}
|
|
48
|
-
export async function dispatchProtocolOp(services, payload, chatId, agents = [], actingAgentId = null,
|
|
48
|
+
export async function dispatchProtocolOp(services, payload, chatId, agents = [], actingAgentId = null, writeKeys = null,
|
|
49
49
|
/** ZIG-1120 #8 — prefer the cached session snapshot over a fresh HTTP read. */
|
|
50
50
|
cachedAgreements) {
|
|
51
51
|
if (!payload?.operation)
|
|
@@ -55,9 +55,9 @@ cachedAgreements) {
|
|
|
55
55
|
const actorId = actingAgentId?.trim() || null;
|
|
56
56
|
switch (operation) {
|
|
57
57
|
case 'agreement-propose':
|
|
58
|
-
return proposeAgreement(services, payload, chatId, agents, actorId,
|
|
58
|
+
return proposeAgreement(services, payload, chatId, agents, actorId, writeKeys);
|
|
59
59
|
case 'agreement-subcontract':
|
|
60
|
-
return subcontractAgreement(services, payload, chatId, agents, actorId,
|
|
60
|
+
return subcontractAgreement(services, payload, chatId, agents, actorId, writeKeys, cachedAgreements);
|
|
61
61
|
case 'agreement-respond':
|
|
62
62
|
if (!payload.agreementId || !payload.action)
|
|
63
63
|
return null;
|
|
@@ -167,7 +167,7 @@ cachedAgreements) {
|
|
|
167
167
|
// creating a duplicate. Keyed on what is actually written (the resolved
|
|
168
168
|
// agreement, not the LLM-cited one), so multiple spawns in one wake stay
|
|
169
169
|
// distinct.
|
|
170
|
-
idempotencyKey:
|
|
170
|
+
idempotencyKey: writeKeys?.taskSpawnKey({
|
|
171
171
|
agreementId: spawnAgreementId,
|
|
172
172
|
description: payload.description,
|
|
173
173
|
assigneeId: payload.assigneeId,
|
|
@@ -184,7 +184,7 @@ cachedAgreements) {
|
|
|
184
184
|
const state = normalizeState(terminalRaw);
|
|
185
185
|
// Wake-derived idempotency key: a crash-replay re-issuing this transition
|
|
186
186
|
// no-ops server-side instead of erroring on the already-terminal task.
|
|
187
|
-
return taskService.updateState(payload.taskId, state, payload.result,
|
|
187
|
+
return taskService.updateState(payload.taskId, state, payload.result, writeKeys?.taskStateKey(payload.taskId, state));
|
|
188
188
|
}
|
|
189
189
|
case 'task-update-plan-step':
|
|
190
190
|
if (!payload.taskId)
|
|
@@ -207,7 +207,7 @@ cachedAgreements) {
|
|
|
207
207
|
throw new Error(`Unknown protocol operation: ${operation}`);
|
|
208
208
|
}
|
|
209
209
|
}
|
|
210
|
-
async function proposeAgreement(services, payload, chatId, _agents, actingAgentId,
|
|
210
|
+
async function proposeAgreement(services, payload, chatId, _agents, actingAgentId, writeKeys = null) {
|
|
211
211
|
if (!payload.description)
|
|
212
212
|
throw new Error('agreement-propose requires `description`');
|
|
213
213
|
if (!payload.proposedTo)
|
|
@@ -248,7 +248,7 @@ async function proposeAgreement(services, payload, chatId, _agents, actingAgentI
|
|
|
248
248
|
// propose resolves to the already-created agreement server-side instead of
|
|
249
249
|
// minting a duplicate proposal. Keyed on what the proposal writes, so
|
|
250
250
|
// multiple proposals in one wake stay distinct.
|
|
251
|
-
const idempotencyKey =
|
|
251
|
+
const idempotencyKey = writeKeys?.agreementProposeKey({
|
|
252
252
|
proposedTo: payload.proposedTo,
|
|
253
253
|
providerId: payload.providerId,
|
|
254
254
|
description: payload.description,
|
|
@@ -301,7 +301,7 @@ function agActive(a) {
|
|
|
301
301
|
function agIsActiveStatus(a) {
|
|
302
302
|
return String(a.status ?? '') === 'active';
|
|
303
303
|
}
|
|
304
|
-
async function subcontractAgreement(services, payload, chatId, _agents, actingAgentId,
|
|
304
|
+
async function subcontractAgreement(services, payload, chatId, _agents, actingAgentId, writeKeys = null, cachedAgreements) {
|
|
305
305
|
if (!payload.description || !payload.executorId)
|
|
306
306
|
return null;
|
|
307
307
|
// b1 (ZIG-832): idempotent subcontract — if a non-terminal sub-agreement for
|
|
@@ -361,7 +361,7 @@ async function subcontractAgreement(services, payload, chatId, _agents, actingAg
|
|
|
361
361
|
// the b1 reuse check above (e.g. no chatId, or the sub not yet visible in the
|
|
362
362
|
// snapshot) resolves to the already-created sub-agreement server-side instead
|
|
363
363
|
// of minting a duplicate.
|
|
364
|
-
const idempotencyKey =
|
|
364
|
+
const idempotencyKey = writeKeys?.agreementSubcontractKey({
|
|
365
365
|
executorId: payload.executorId,
|
|
366
366
|
description: payload.description,
|
|
367
367
|
parentAgreementId: payload.parentAgreementId ?? '',
|
|
@@ -36,7 +36,7 @@ function handlerFor(toolName) {
|
|
|
36
36
|
taskService: c.taskService,
|
|
37
37
|
agreementService: c.agreementService,
|
|
38
38
|
contextReadClient: c.contextReadClient,
|
|
39
|
-
}, { operation, ...args }, c.chatId ?? null, c.agents ?? [], typeof c.agentId === 'string' ? c.agentId : null, c.
|
|
39
|
+
}, { operation, ...args }, c.chatId ?? null, c.agents ?? [], typeof c.agentId === 'string' ? c.agentId : null, c.writeKeys ?? null, Array.isArray(c.agreements) ? c.agreements : undefined);
|
|
40
40
|
if (result == null)
|
|
41
41
|
throw new Error(`Operation ${operation} returned no result`);
|
|
42
42
|
return result;
|
package/dist/types.d.ts
CHANGED
|
@@ -197,7 +197,7 @@ export interface Ctx {
|
|
|
197
197
|
* traffic could drive it, the brain skips the LLM instead of burning the
|
|
198
198
|
* fleet key on a wake storm.
|
|
199
199
|
*/
|
|
200
|
-
|
|
200
|
+
recentDeliveries?: number[];
|
|
201
201
|
}
|
|
202
202
|
export interface PromptDef {
|
|
203
203
|
role?: string;
|
|
@@ -369,11 +369,11 @@ export interface RecordedEntry {
|
|
|
369
369
|
/**
|
|
370
370
|
* Deterministic idempotency keys for one wake's writes (delivery law, item 2).
|
|
371
371
|
* Created once per tick from (laneKey, triggering event) — see
|
|
372
|
-
* `
|
|
372
|
+
* `createDeliveryWriteKeys` — and threaded to every artifact / task-state write the
|
|
373
373
|
* wake performs, so a crash-replay of the same wake reproduces the same keys
|
|
374
374
|
* and the backend drops the duplicates. Never LLM-supplied, never random.
|
|
375
375
|
*/
|
|
376
|
-
export interface
|
|
376
|
+
export interface DeliveryWriteKeys {
|
|
377
377
|
/** Stable id of this wake, or null when its trigger has no durable delivery identity (timer/synthetic frames) — null means writes go keyless, as before. */
|
|
378
378
|
readonly wakeKey: string | null;
|
|
379
379
|
/**
|
|
@@ -463,7 +463,7 @@ export type Effect = {
|
|
|
463
463
|
/** FSM state name (ZIG-440). */
|
|
464
464
|
stateId?: string;
|
|
465
465
|
/** Replay-stable write keys for this wake — write-capable tools derive their idempotency key from here, never from the LLM. */
|
|
466
|
-
|
|
466
|
+
writeKeys?: DeliveryWriteKeys;
|
|
467
467
|
} | {
|
|
468
468
|
kind: 'send-message';
|
|
469
469
|
sessionId: string;
|