@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 +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 +24 -4
- 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/tools/ToolManager.js +20 -1
- package/dist/tools/defineTool.js +8 -1
- package/dist/types.d.ts +4 -4
- package/package.json +2 -2
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,
|
|
@@ -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
|
-
|
|
75
|
-
|
|
76
|
-
|
|
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
|
-
|
|
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 {
|
|
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;
|
|
@@ -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}": ${
|
|
90
|
+
throw makeError(`Invalid args for "${name}": ${describeErrors(parameters, validate.errors)}`, 'validation_error', { data: { errors: validate.errors } });
|
|
72
91
|
}
|
|
73
92
|
}
|
|
74
93
|
}
|
package/dist/tools/defineTool.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ziggs-ai/agent-sdk",
|
|
3
|
-
"version": "0.10.
|
|
3
|
+
"version": "0.10.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.
|
|
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",
|