@ziggs-ai/agent-sdk 0.10.1 → 0.10.3

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 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
 
@@ -1,5 +1,5 @@
1
1
  import { AgentMachine } from '../runtime/AgentMachine.js';
2
- import { createWakeWriteKeys } from '../runtime/wakeWriteKeys.js';
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
- wakeKeys: createWakeWriteKeys(identity.laneKey, event),
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 { createWakeWriteKeys } from '../runtime/wakeWriteKeys.js';
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 recentWakes = (nextCtx.recentWakes ?? []).filter((t) => now - t < WAKE_WINDOW_MS);
226
- if (recentWakes.length >= MAX_WAKES_PER_WINDOW) {
227
- runtimeLog.warn('ClaudeSdkAgent', `wake-rate breaker tripped: ${recentWakes.length} wakes in ${WAKE_WINDOW_MS / 60_000}min ` +
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.recentWakes = recentWakes;
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.recentWakes = [...recentWakes, now];
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
- const systemPrompt = `${CLAUDE_SDK_PROTOCOL}\n\n${roleBlock}`;
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 wakeKeys = createWakeWriteKeys(identity.laneKey, rawEvent);
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
- wakeKeys,
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 = wakeKeys.artifactKey({ kind: 'thought', text });
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 { Options, SDKAssistantMessage } from '@anthropic-ai/claude-agent-sdk';
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
- /** Protocol + role system prompt. */
18
- systemPrompt: string;
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, WakeWriteKeys } from '../types.js';
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
- wakeKeys?: WakeWriteKeys;
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.wakeKeys?.messageKey({ text, sessionId, receiverId }) ?? `sdk_${randomUUID()}`;
80
+ : c.writeKeys?.messageKey({ text, sessionId, receiverId }) ?? `sdk_${randomUUID()}`;
81
81
  return c.eff({
82
82
  kind: 'send-message',
83
83
  sessionId,
@@ -53,6 +53,25 @@ function parametersToZodShape(parameters) {
53
53
  }
54
54
  return shape;
55
55
  }
56
+ /**
57
+ * ZIG-1215 — the Claude brain's args are validated here, before `eff` hands
58
+ * them to the shared executor, so a plain shape means an unknown param is
59
+ * stripped at this layer and ToolManager's own check never sees it. Closing
60
+ * the object is what makes the rejection reach the brain: the call fails with
61
+ * the offending key named, instead of succeeding on a request it silently
62
+ * altered. The published schema also carries `additionalProperties: false`,
63
+ * so the typo is usually caught before the call is made at all.
64
+ */
65
+ function strictParams(toolName, shape) {
66
+ const accepted = Object.keys(shape).join(', ') || '(none)';
67
+ return z.strictObject(shape, {
68
+ error: (issue) => issue.code === 'unrecognized_keys'
69
+ ? `Unknown parameter(s) ${issue.keys.map((k) => `"${k}"`).join(', ')} — ` +
70
+ `${toolName} did not run. Accepted params: ${accepted}. ` +
71
+ `Check for a typo and call again; nothing was changed.`
72
+ : undefined,
73
+ });
74
+ }
56
75
  function toCallToolResult(value) {
57
76
  const text = typeof value === 'string' ? value : JSON.stringify(value, null, 2);
58
77
  return { content: [{ type: 'text', text }] };
@@ -71,9 +90,10 @@ export function buildSdkToolDefs(toolDefs, getContext) {
71
90
  const name = String(fn['name'] ?? '');
72
91
  const description = String(fn['description'] ?? name);
73
92
  const shape = parametersToZodShape(fn['parameters']);
74
- const inputSchema = Object.keys(shape).length > 0
75
- ? shape
76
- : { _: z.record(z.string(), z.unknown()).optional() };
93
+ // A zero-param tool keeps the `_` envelope the Claude SDK needs, but the
94
+ // envelope itself is closed too — its free-form payload is still checked
95
+ // against the tool's own schema downstream in ToolManager.
96
+ const inputSchema = strictParams(name, Object.keys(shape).length > 0 ? shape : { _: z.record(z.string(), z.unknown()).optional() });
77
97
  return {
78
98
  name,
79
99
  description,
@@ -100,7 +120,7 @@ export function buildSdkToolDefs(toolDefs, getContext) {
100
120
  senderType: ctx.senderType,
101
121
  onBehalfOfAgreementId: ctx.onBehalfOfAgreementId,
102
122
  runId: ctx.runId,
103
- wakeKeys: ctx.wakeKeys,
123
+ writeKeys: ctx.writeKeys,
104
124
  });
105
125
  if (r.ok)
106
126
  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 { createWakeWriteKeys } from './runtime/wakeWriteKeys.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, WakeWriteKeys, } from './types.js';
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 { createWakeWriteKeys } from './runtime/wakeWriteKeys.js';
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, } from '@ziggs-ai/api-client';
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 (wakeWriteKeys).
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
- * wakeWriteKeys depend on, nothing more. `ownAgentId` is accepted for call-site
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
- * wakeWriteKeys depend on, nothing more. `ownAgentId` is accepted for call-site
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, WakeWriteKeys } from '../types.js';
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
- wakeKeys?: WakeWriteKeys;
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
- wakeKeys?: WakeWriteKeys;
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, wakeKeys }: MachineOptions);
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
- wakeKeys;
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, wakeKeys }) {
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.wakeKeys = wakeKeys;
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
- wakeKeys: this.wakeKeys,
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
- wakeKeys: this.wakeKeys,
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.wakeKeys?.messageKey({ text, sessionId, receiverId: userId }),
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 { WakeWriteKeys } from '../types.js';
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 createWakeWriteKeys(laneKey: string, event: unknown): WakeWriteKeys;
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 createWakeWriteKeys(laneKey, event) {
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, WakeWriteKeys } from '../types.js';
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
- wakeKeys?: WakeWriteKeys;
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, wakeKeys } = input;
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, wakeKeys });
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, WakeWriteKeys } from '../types.js';
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
- wakeKeys?: WakeWriteKeys;
36
+ writeKeys?: DeliveryWriteKeys;
37
37
  }
38
38
  interface RunTurnResult {
39
39
  outcome: Outcome;
@@ -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, wakeKeys } = input;
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, wakeKeys });
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: wakeKeys?.messageKey({
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: wakeKeys?.artifactKey({ kind: 'thought', text: s }) });
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
- wakeKeys?.messageKey({
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
- sleep(agentId: string): Promise<void>;
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 sleep(agentId) {
76
- return this.manager.sleep(agentId);
75
+ async stopAgent(agentId) {
76
+ return this.manager.stopAgent(agentId);
77
77
  }
78
78
  async disconnectAll() {
79
- return this.manager.sleepAll();
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.wakeKeys?.artifactKey({ s: 'started', t: effect.name, a: argsPreview }));
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
- wakeKeys: effect.wakeKeys,
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.wakeKeys?.artifactKey({ s: 'completed', t: effect.name, r: clipped }));
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.wakeKeys?.artifactKey({ s: 'failed', t: effect.name, e: errMsg }));
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-wake\/([^/?]+)/);
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 { WakeWriteKeys } from '../../../types.js';
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, wakeKeys?: WakeWriteKeys | 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, wakeKeys = 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, wakeKeys);
58
+ return proposeAgreement(services, payload, chatId, agents, actorId, writeKeys);
59
59
  case 'agreement-subcontract':
60
- return subcontractAgreement(services, payload, chatId, agents, actorId, wakeKeys, cachedAgreements);
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: wakeKeys?.taskSpawnKey({
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, wakeKeys?.taskStateKey(payload.taskId, state));
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, wakeKeys = null) {
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 = wakeKeys?.agreementProposeKey({
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, wakeKeys = null, cachedAgreements) {
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 = wakeKeys?.agreementSubcontractKey({
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.wakeKeys ?? null, Array.isArray(c.agreements) ? c.agreements : undefined);
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;
@@ -9,6 +9,25 @@ function makeError(message, type, extra) {
9
9
  Object.assign(err, extra);
10
10
  return err;
11
11
  }
12
+ /**
13
+ * ZIG-1215 — Ajv's own wording for a rejected unknown param is "must NOT have
14
+ * additional properties", which names neither the offending key nor the ones
15
+ * that would have worked. An agent reading that has nothing to correct against,
16
+ * so the typo is named alongside the menu it should have come from.
17
+ */
18
+ function describeErrors(parameters, errors) {
19
+ const unknown = (errors ?? [])
20
+ .map((e) => e)
21
+ .filter((e) => e.keyword === 'additionalProperties')
22
+ .map((e) => e.params?.additionalProperty)
23
+ .filter((k) => typeof k === 'string');
24
+ if (unknown.length === 0)
25
+ return ajv.errorsText(errors);
26
+ const props = parameters.properties ?? {};
27
+ const accepted = Object.keys(props).join(', ') || '(none)';
28
+ return (`unknown parameter(s) ${unknown.map((k) => `"${k}"`).join(', ')} — ` +
29
+ `accepted params: ${accepted}. Check for a typo and call again; nothing was changed.`);
30
+ }
12
31
  function stripNullish(args) {
13
32
  if (!args || typeof args !== 'object' || Array.isArray(args))
14
33
  return args;
@@ -68,7 +87,7 @@ export class ToolManager extends ToolProvider {
68
87
  const validate = ajv.compile(parameters);
69
88
  if (!validate(args)) {
70
89
  const name = tool.schema.function?.['name'];
71
- throw makeError(`Invalid args for "${name}": ${ajv.errorsText(validate.errors)}`, 'validation_error', { data: { errors: validate.errors } });
90
+ throw makeError(`Invalid args for "${name}": ${describeErrors(parameters, validate.errors)}`, 'validation_error', { data: { errors: validate.errors } });
72
91
  }
73
92
  }
74
93
  }
@@ -48,7 +48,14 @@ export function defineTool(name, params, handler, options = {}) {
48
48
  throw new Error('defineTool: name is required');
49
49
  if (!handler || typeof handler !== 'function')
50
50
  throw new Error('defineTool: handler function is required');
51
- const parameters = convertObject(params || {});
51
+ // ZIG-1215 — the top level is closed so a misspelled param is rejected by
52
+ // ToolManager's Ajv pass instead of being dropped on the floor. JSON Schema
53
+ // allows unknown keys by default, so without this the validation ran and
54
+ // passed while the handler quietly ignored what the model asked for.
55
+ // Nested objects stay open: the typo class this guards against is at the
56
+ // call surface the model writes, and closing nested payloads (task result,
57
+ // plan) would reject shapes the backend accepts.
58
+ const parameters = { ...convertObject(params || {}), additionalProperties: false };
52
59
  const fn = { name, parameters };
53
60
  if (options.description)
54
61
  fn['description'] = options.description;
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
- recentWakes?: number[];
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
- * `createWakeWriteKeys` — and threaded to every artifact / task-state write the
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 WakeWriteKeys {
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
- wakeKeys?: WakeWriteKeys;
466
+ writeKeys?: DeliveryWriteKeys;
467
467
  } | {
468
468
  kind: 'send-message';
469
469
  sessionId: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ziggs-ai/agent-sdk",
3
- "version": "0.10.1",
3
+ "version": "0.10.3",
4
4
  "description": "Agent framework SDK for building autonomous agents on the Ziggs platform",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -26,7 +26,7 @@
26
26
  "@anthropic-ai/claude-agent-sdk": "^0.3.204",
27
27
  "@anthropic-ai/sdk": "^0.110.0",
28
28
  "@modelcontextprotocol/sdk": "^1.29.0",
29
- "@ziggs-ai/api-client": "^0.9.1",
29
+ "@ziggs-ai/api-client": "^0.9.2",
30
30
  "ajv": "^8.17.1",
31
31
  "dotenv": "^17.2.3",
32
32
  "openai": "^4.0.0",