@ziggs-ai/agent-sdk 0.2.1 → 0.3.0
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 +1 -1
- package/dist/AgentHost.d.ts +21 -20
- package/dist/AgentHost.js +60 -136
- package/dist/adapters/McpAdapter.js +14 -13
- package/dist/agent/Agent.js +4 -0
- package/dist/agent/ClaudeSdkAgent.d.ts +5 -0
- package/dist/agent/ClaudeSdkAgent.js +25 -6
- package/dist/agent/claudeCognition.d.ts +7 -0
- package/dist/agent/claudeCognition.js +38 -7
- package/dist/agent/claudeSdkProtocol.d.ts +8 -7
- package/dist/agent/claudeSdkProtocol.js +8 -7
- package/dist/agent/claudeSdkTools.d.ts +4 -2
- package/dist/agent/claudeSdkTools.js +26 -6
- package/dist/agent/toolManagerToSdkMcp.js +1 -0
- package/dist/index.d.ts +5 -9
- package/dist/index.js +12 -6
- package/dist/ingress/normalizeIncoming.d.ts +17 -3
- package/dist/ingress/normalizeIncoming.js +11 -26
- package/dist/runtime/AgentMachine.d.ts +5 -2
- package/dist/runtime/AgentMachine.js +8 -2
- package/dist/runtime/runMechanical.d.ts +3 -1
- package/dist/runtime/runMechanical.js +2 -2
- package/dist/runtime/runTurn.d.ts +3 -1
- package/dist/runtime/runTurn.js +25 -7
- package/dist/runtime/wakeWriteKeys.d.ts +25 -0
- package/dist/runtime/wakeWriteKeys.js +134 -0
- package/dist/server/ConnectionPool.d.ts +7 -4
- package/dist/server/ConnectionPool.js +11 -14
- package/dist/server/ZiggsEffectHandler.js +17 -12
- package/dist/server/inboxLoop.d.ts +50 -0
- package/dist/server/{InboxCatchUp.js → inboxLoop.js} +63 -52
- package/dist/server/runLauncher.js +1 -1
- package/dist/server/tasks/TaskService.d.ts +2 -1
- package/dist/server/tasks/TaskService.js +5 -2
- package/dist/server/tools/index.d.ts +7 -3
- package/dist/server/tools/index.js +13 -3
- package/dist/server/tools/tier1/protocolRunner.d.ts +2 -1
- package/dist/server/tools/tier1/protocolRunner.js +43 -9
- package/dist/server/tools/tier1/protocolTools.js +1 -1
- package/dist/server/tools/tier2/artifactTools.d.ts +3 -0
- package/dist/server/tools/tier2/artifactTools.js +82 -0
- package/dist/server/tools/tier2/chatTools.d.ts +3 -0
- package/dist/server/tools/tier2/chatTools.js +37 -0
- package/dist/server/tools/tier2/connectionTools.d.ts +1 -0
- package/dist/server/tools/tier2/connectionTools.js +52 -5
- package/dist/server/tools/tier2/contextTools.d.ts +4 -2
- package/dist/server/tools/tier2/contextTools.js +105 -12
- package/dist/server/tools/tier2/grantsTools.d.ts +3 -0
- package/dist/server/tools/tier2/grantsTools.js +81 -0
- package/dist/server/tools/tier2/linkTools.d.ts +7 -0
- package/dist/server/tools/tier2/linkTools.js +156 -0
- package/dist/server/tools/tier2/paymentTools.d.ts +0 -1
- package/dist/server/tools/tier2/paymentTools.js +9 -13
- package/dist/types.d.ts +40 -0
- package/package.json +2 -2
- package/dist/server/InboxCatchUp.d.ts +0 -24
- package/dist/server/ziggsconnect/ZiggsConnectClient.d.ts +0 -35
- package/dist/server/ziggsconnect/ZiggsConnectClient.js +0 -95
- package/dist/server/ziggscontext/ZiggsContextClient.d.ts +0 -53
- package/dist/server/ziggscontext/ZiggsContextClient.js +0 -124
- package/dist/server/ziggspay/ZiggsPayClient.d.ts +0 -82
- package/dist/server/ziggspay/ZiggsPayClient.js +0 -203
package/README.md
CHANGED
|
@@ -105,7 +105,7 @@ Examples in the repo: `examples/agents/*.js` (e.g. coffee, expense, delivery age
|
|
|
105
105
|
- **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
106
|
- **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
107
|
- **Adapters / utils:** `OpenAIAdapter`, JSON helpers, formatters
|
|
108
|
-
- **Re-exported** from `@ziggs-ai/api-client`: `
|
|
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`.)
|
|
109
109
|
|
|
110
110
|
Package export: `"."` → `dist/index.js` (see `package.json`; `npm run build` before publish or monorepo tests).
|
|
111
111
|
|
package/dist/AgentHost.d.ts
CHANGED
|
@@ -25,7 +25,6 @@ export interface SessionStore {
|
|
|
25
25
|
import { ToolManager } from './tools/ToolManager.js';
|
|
26
26
|
import type { ToolDefinition } from './tools/defineTool.js';
|
|
27
27
|
import { OpenAIAdapter } from './adapters/OpenAIAdapter.js';
|
|
28
|
-
import { WebSocketClient } from '@ziggs-ai/api-client';
|
|
29
28
|
/**
|
|
30
29
|
* The single fleet-wide default model — the one place the fallback lives.
|
|
31
30
|
* Change this (or set `LLM_MODEL`) to move the whole fleet; nothing hardcodes a
|
|
@@ -75,6 +74,15 @@ export interface AgentHostOptions {
|
|
|
75
74
|
cognition?: CognitionKind;
|
|
76
75
|
/** Max agentic turns per Claude SDK wake. Default 30. */
|
|
77
76
|
claudeMaxTurns?: number;
|
|
77
|
+
/**
|
|
78
|
+
* ZIG-860: claude-sdk agents resume their transcript across wakes by default,
|
|
79
|
+
* so per-wake input grows monotonically on a long lane. When true, each wake
|
|
80
|
+
* starts a fresh Claude session — the protocol is backend-authoritative (the
|
|
81
|
+
* brain re-reads via ziggs_context_snapshot / the wake event), so the
|
|
82
|
+
* transcript is convenience memory, not source of truth. Ideal for
|
|
83
|
+
* step-driven orchestrators whose every wake already carries what it needs.
|
|
84
|
+
*/
|
|
85
|
+
claudeStatelessPerWake?: boolean;
|
|
78
86
|
services?: AgentServices;
|
|
79
87
|
workflow?: Workflow;
|
|
80
88
|
states?: Workflow['states'];
|
|
@@ -133,7 +141,6 @@ export declare class AgentHost {
|
|
|
133
141
|
toolManager: ToolManager;
|
|
134
142
|
llm?: OpenAIAdapter | LlmAdapter;
|
|
135
143
|
agent: Agent | ClaudeSdkAgent;
|
|
136
|
-
wsClient: WebSocketClient;
|
|
137
144
|
private eff;
|
|
138
145
|
private eventQueue;
|
|
139
146
|
private sessions;
|
|
@@ -145,39 +152,33 @@ export declare class AgentHost {
|
|
|
145
152
|
private agreementService;
|
|
146
153
|
private messageSender;
|
|
147
154
|
private seenMessages;
|
|
148
|
-
/** Lazy inbox client for advancing the watermark on the live-push path (ZIG-832 P4). */
|
|
149
|
-
private _inbox?;
|
|
150
155
|
private stateTimers;
|
|
151
|
-
|
|
152
|
-
private
|
|
156
|
+
/** The per-agent inbox long-poll loop — this agent's sole wake source. */
|
|
157
|
+
private inboxLoop;
|
|
158
|
+
private inboxLoopEnabled;
|
|
153
159
|
constructor(options?: AgentHostOptions);
|
|
154
160
|
private _validate;
|
|
155
161
|
private _setup;
|
|
156
162
|
/**
|
|
157
|
-
*
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
/**
|
|
162
|
-
* Push notification handler — invoked when the backend sees a relevant
|
|
163
|
-
* resource change (artifact created, task state transition, agreement
|
|
164
|
-
* updated). The agent decides which session/lane to wake based on the
|
|
165
|
-
* event payload; it pulls the actual data via the primitive endpoints
|
|
166
|
-
* from inside the tick.
|
|
163
|
+
* Handler for a resource change (artifact created, task state transition,
|
|
164
|
+
* agreement updated) surfaced by the inbox loop. The agent decides which
|
|
165
|
+
* session/lane to route it to from the event payload, then pulls the actual
|
|
166
|
+
* data via the primitive endpoints from inside the tick.
|
|
167
167
|
*
|
|
168
|
+
* No client-side relevance or echo guard: event targeting is backend policy
|
|
169
|
+
* now (the law never routes an agent its own writes), and the inbox loop is a
|
|
170
|
+
* single delta-read channel — there is no second channel to dedup against.
|
|
168
171
|
* Lightweight: defers all work to a tick by enqueueing a synthetic event
|
|
169
|
-
* carrying just enough context
|
|
172
|
+
* carrying just enough context to route it.
|
|
170
173
|
*/
|
|
171
174
|
handleResourceEvent(event: Record<string, unknown>): Promise<void>;
|
|
172
175
|
/** Wire entry. Normalize → resolve session → enqueue on the right lane. */
|
|
173
176
|
handleMessage(text: string, metadata?: Record<string, unknown>): Promise<void>;
|
|
174
|
-
/** Best-effort: advance the inbox watermark for a chat handled live (ZIG-832 P4). */
|
|
175
|
-
private _ackChatWatermark;
|
|
176
177
|
private _processEvent;
|
|
177
178
|
/** Arms the lane's liveness timer iff stateName is a parked state declaring timeoutMs. */
|
|
178
179
|
private _armIfTimedParked;
|
|
179
180
|
private _resolveOwnAgentId;
|
|
180
|
-
connectAsync(
|
|
181
|
+
connectAsync(_timeout?: number): Promise<this>;
|
|
181
182
|
private _bootstrapAssignedActiveTasks;
|
|
182
183
|
/** ZIG-802: replay task-assigned for an existing active task (demo recovery). */
|
|
183
184
|
wakeTaskAssignment(taskId: string): Promise<void>;
|
package/dist/AgentHost.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
1
2
|
import { Agent } from './agent/Agent.js';
|
|
2
3
|
import { ClaudeSdkAgent } from './agent/ClaudeSdkAgent.js';
|
|
3
4
|
import { applyBrainToHostOptions, isClaudeCognition, } from './brain/index.js';
|
|
@@ -13,10 +14,10 @@ import { ToolManager } from './tools/ToolManager.js';
|
|
|
13
14
|
import { PROTOCOL_TOOLS } from './server/tools/tier1/protocolTools.js';
|
|
14
15
|
import { OpenAIAdapter } from './adapters/OpenAIAdapter.js';
|
|
15
16
|
import { AnthropicAdapter } from './adapters/AnthropicAdapter.js';
|
|
16
|
-
import {
|
|
17
|
+
import { ArtifactsClient, ContextReadClient, TelemetryClient, sendChatMessage, } from '@ziggs-ai/api-client';
|
|
17
18
|
import { TaskService } from './server/tasks/TaskService.js';
|
|
18
19
|
import { AgreementService } from './server/agreements/AgreementService.js';
|
|
19
|
-
import {
|
|
20
|
+
import { runInboxLoop } from './server/inboxLoop.js';
|
|
20
21
|
import { SeenMessages } from './server/SeenMessages.js';
|
|
21
22
|
import { runtimeLog } from './shared/runtimeLog.js';
|
|
22
23
|
/**
|
|
@@ -100,7 +101,6 @@ export class AgentHost {
|
|
|
100
101
|
// drives query() directly and needs no LLM adapter.
|
|
101
102
|
llm;
|
|
102
103
|
agent;
|
|
103
|
-
wsClient;
|
|
104
104
|
eff;
|
|
105
105
|
eventQueue;
|
|
106
106
|
sessions = new Map();
|
|
@@ -112,11 +112,10 @@ export class AgentHost {
|
|
|
112
112
|
agreementService;
|
|
113
113
|
messageSender;
|
|
114
114
|
seenMessages = new SeenMessages();
|
|
115
|
-
/** Lazy inbox client for advancing the watermark on the live-push path (ZIG-832 P4). */
|
|
116
|
-
_inbox;
|
|
117
115
|
stateTimers;
|
|
118
|
-
|
|
119
|
-
|
|
116
|
+
/** The per-agent inbox long-poll loop — this agent's sole wake source. */
|
|
117
|
+
inboxLoop = null;
|
|
118
|
+
inboxLoopEnabled;
|
|
120
119
|
constructor(options = {}) {
|
|
121
120
|
const resolved = applyBrainToHostOptions(options);
|
|
122
121
|
this.options = {
|
|
@@ -144,8 +143,9 @@ export class AgentHost {
|
|
|
144
143
|
brain: resolved.brain,
|
|
145
144
|
cognition: resolved.cognition ?? 'fsm',
|
|
146
145
|
claudeMaxTurns: resolved.claudeMaxTurns,
|
|
146
|
+
claudeStatelessPerWake: resolved.claudeStatelessPerWake,
|
|
147
147
|
};
|
|
148
|
-
this.
|
|
148
|
+
this.inboxLoopEnabled = options.inboxCatchUp !== false;
|
|
149
149
|
this.sessionResolver = options.sessionResolver || DEFAULT_SESSION_RESOLVER;
|
|
150
150
|
this.tickTimeoutMs = options.tickTimeoutMs ?? 120_000;
|
|
151
151
|
this.memory = options.memory ?? new InMemoryStore();
|
|
@@ -177,19 +177,24 @@ export class AgentHost {
|
|
|
177
177
|
const services = createBackendServices(this.options);
|
|
178
178
|
this.toolManager = services.toolManager;
|
|
179
179
|
this.llm = services.llm;
|
|
180
|
-
|
|
181
|
-
|
|
180
|
+
// Outbound send is HTTP (delivery law, phase 4): the agent no longer holds a
|
|
181
|
+
// socket. `POST /chats/:id/messages` persists the whole message — the same
|
|
182
|
+
// row the WS path persisted. Live-token streaming had no HTTP path, so
|
|
183
|
+
// `chunkSender` is intentionally dropped: partials degrade, the full reply
|
|
184
|
+
// is never lost (runTurn always follows a stream with a whole send-message).
|
|
185
|
+
const creds = {
|
|
182
186
|
operatorKey: this.options.operatorKey,
|
|
183
187
|
agentId: this.options.agentId,
|
|
184
|
-
label: this.options.name || 'agent',
|
|
185
|
-
});
|
|
186
|
-
const messageSender = (message, receiverId, sessionId, messageId) => {
|
|
187
|
-
this.wsClient.send(sessionId, receiverId, message, messageId ? { messageId } : undefined);
|
|
188
|
-
return Promise.resolve(undefined);
|
|
189
|
-
};
|
|
190
|
-
const chunkSender = (sessionId, receiverId, text, messageId) => {
|
|
191
|
-
this.wsClient.send(sessionId, receiverId, text, { partial: true, messageId });
|
|
192
188
|
};
|
|
189
|
+
const messageSender = (message, receiverId, sessionId, messageId) => sendChatMessage({
|
|
190
|
+
chatId: sessionId,
|
|
191
|
+
receiverId,
|
|
192
|
+
text: message,
|
|
193
|
+
// messageId is the backend's (chatId, messageId) dedup key. Callers
|
|
194
|
+
// pass a wake-derived id for replay-stability; fall back to a fresh id
|
|
195
|
+
// only when absent (matches the old WS path letting the server assign).
|
|
196
|
+
messageId: messageId ?? `sdk_${randomUUID()}`,
|
|
197
|
+
}, creds);
|
|
193
198
|
this.taskService = services.taskService;
|
|
194
199
|
this.agreementService = services.agreementService;
|
|
195
200
|
this.messageSender = messageSender;
|
|
@@ -225,7 +230,6 @@ export class AgentHost {
|
|
|
225
230
|
taskService: services.taskService,
|
|
226
231
|
agreementService: services.agreementService,
|
|
227
232
|
messageSender,
|
|
228
|
-
chunkSender,
|
|
229
233
|
toolManager: services.toolManager,
|
|
230
234
|
operatorKey: this.options.operatorKey,
|
|
231
235
|
agentId: this.options.agentId,
|
|
@@ -247,6 +251,7 @@ export class AgentHost {
|
|
|
247
251
|
// eff({ kind: 'tool-call' }) → the shared executor, which sources those
|
|
248
252
|
// services from the effect handler's deps like the FSM path (ZIG-775).
|
|
249
253
|
maxTurns: this.options.claudeMaxTurns,
|
|
254
|
+
statelessPerWake: this.options.claudeStatelessPerWake,
|
|
250
255
|
})
|
|
251
256
|
: new Agent({
|
|
252
257
|
workflow,
|
|
@@ -255,54 +260,18 @@ export class AgentHost {
|
|
|
255
260
|
});
|
|
256
261
|
this.eventQueue = new EventQueue((event, laneKey) => this._processEvent(event, laneKey));
|
|
257
262
|
this.stateTimers = new StateTimers((ev, laneKey) => { void this.eventQueue.enqueue(ev, laneKey); });
|
|
258
|
-
this.wsClient.setMessageHandler((text, metadata) => this.handleMessage(text, metadata));
|
|
259
|
-
this.wsClient.setResourceEventHandler((event) => this.handleResourceEvent(event));
|
|
260
|
-
if (this.inboxCatchUpEnabled) {
|
|
261
|
-
this.wsClient.onConnect(() => {
|
|
262
|
-
void this._ensureCatchUp();
|
|
263
|
-
});
|
|
264
|
-
}
|
|
265
|
-
}
|
|
266
|
-
/**
|
|
267
|
-
* Coalesce concurrent connect/reconnect bursts into one catch-up pass.
|
|
268
|
-
*/
|
|
269
|
-
_ensureCatchUp() {
|
|
270
|
-
if (!this.catchUpInFlight) {
|
|
271
|
-
this.catchUpInFlight = this._runInboxCatchUp().finally(() => {
|
|
272
|
-
this.catchUpInFlight = null;
|
|
273
|
-
});
|
|
274
|
-
}
|
|
275
|
-
return this.catchUpInFlight;
|
|
276
|
-
}
|
|
277
|
-
async _runInboxCatchUp() {
|
|
278
|
-
const label = this.options.name || 'agent';
|
|
279
|
-
try {
|
|
280
|
-
await runInboxCatchUp({
|
|
281
|
-
creds: {
|
|
282
|
-
operatorKey: this.options.operatorKey,
|
|
283
|
-
agentId: this.options.agentId,
|
|
284
|
-
},
|
|
285
|
-
ownAgentId: this.options.agentId,
|
|
286
|
-
baseUrl: this.options.wsUrl ? undefined : undefined,
|
|
287
|
-
seen: this.seenMessages,
|
|
288
|
-
label,
|
|
289
|
-
onMessage: (text, metadata) => this.handleMessage(text, metadata),
|
|
290
|
-
onResourceEvent: (event) => this.handleResourceEvent(event),
|
|
291
|
-
});
|
|
292
|
-
}
|
|
293
|
-
catch (err) {
|
|
294
|
-
runtimeLog.warn(label, `inbox catch-up failed: ${err?.message ?? err}`);
|
|
295
|
-
}
|
|
296
263
|
}
|
|
297
264
|
/**
|
|
298
|
-
*
|
|
299
|
-
*
|
|
300
|
-
*
|
|
301
|
-
*
|
|
302
|
-
* from inside the tick.
|
|
265
|
+
* Handler for a resource change (artifact created, task state transition,
|
|
266
|
+
* agreement updated) surfaced by the inbox loop. The agent decides which
|
|
267
|
+
* session/lane to route it to from the event payload, then pulls the actual
|
|
268
|
+
* data via the primitive endpoints from inside the tick.
|
|
303
269
|
*
|
|
270
|
+
* No client-side relevance or echo guard: event targeting is backend policy
|
|
271
|
+
* now (the law never routes an agent its own writes), and the inbox loop is a
|
|
272
|
+
* single delta-read channel — there is no second channel to dedup against.
|
|
304
273
|
* Lightweight: defers all work to a tick by enqueueing a synthetic event
|
|
305
|
-
* carrying just enough context
|
|
274
|
+
* carrying just enough context to route it.
|
|
306
275
|
*/
|
|
307
276
|
async handleResourceEvent(event) {
|
|
308
277
|
if (!event || typeof event !== 'object')
|
|
@@ -310,29 +279,6 @@ export class AgentHost {
|
|
|
310
279
|
const ownAgentId = this.options.agentId;
|
|
311
280
|
if (!ownAgentId)
|
|
312
281
|
return;
|
|
313
|
-
// The agent's own writes aren't news to it (same rule the backend inbox
|
|
314
|
-
// applies to counts). The backend already skips agent actors on emit;
|
|
315
|
-
// this guards the same invariant host-side so a self-caused event can
|
|
316
|
-
// never re-wake the brain — that echo was a token-burning wake loop for
|
|
317
|
-
// LLM cognitions (one wake's tool calls seeded the next wake, forever).
|
|
318
|
-
if (event.actorId != null && String(event.actorId) === ownAgentId) {
|
|
319
|
-
runtimeLog.debug('AgentHost', `skip self-caused resource event kind=${event.kind ?? 'unknown'} resourceId=${event.resourceId ?? 'null'}`);
|
|
320
|
-
return;
|
|
321
|
-
}
|
|
322
|
-
// A `message` resource event is the second channel for news the message
|
|
323
|
-
// push already delivered — the backend emits BOTH a `chat:message` frame
|
|
324
|
-
// and a `resource_changed {kind:'message', resourceId: messageId}` for
|
|
325
|
-
// every send. The resource frame exists for offline recovery (queue +
|
|
326
|
-
// launcher wake); when we already processed that messageId live via
|
|
327
|
-
// handleMessage, waking again just re-runs the brain over handled news
|
|
328
|
-
// (every inbound chat message cost a second wake). Unseen ids still wake:
|
|
329
|
-
// that's the recovery path working.
|
|
330
|
-
if (event.kind === 'message' &&
|
|
331
|
-
typeof event.resourceId === 'string' &&
|
|
332
|
-
this.seenMessages.has(event.resourceId)) {
|
|
333
|
-
runtimeLog.debug('AgentHost', `skip resource event for already-handled message ${event.resourceId}`);
|
|
334
|
-
return;
|
|
335
|
-
}
|
|
336
282
|
// Map the event to a candidate session key using the configured resolver
|
|
337
283
|
// on a synthetic normalized-event shape. Default resolver buckets by
|
|
338
284
|
// chatId; agents using a different sessionResolver (agreementId, taskId,
|
|
@@ -341,8 +287,6 @@ export class AgentHost {
|
|
|
341
287
|
const fakeNormalized = {
|
|
342
288
|
chatId: event.chatId,
|
|
343
289
|
event: { type: 'message', timestamp: Date.now(), text: '' },
|
|
344
|
-
shouldProcess: true,
|
|
345
|
-
reason: null,
|
|
346
290
|
};
|
|
347
291
|
const sessionId = this.sessionResolver(fakeNormalized, ownAgentId);
|
|
348
292
|
if (!sessionId)
|
|
@@ -375,12 +319,10 @@ export class AgentHost {
|
|
|
375
319
|
enrichedText = String(terms.description);
|
|
376
320
|
}
|
|
377
321
|
}
|
|
322
|
+
// Classification + shape only. Targeting is backend policy now — the inbox
|
|
323
|
+
// loop delivers only what is fenced to this agent, so there is no
|
|
324
|
+
// client-side "is this for me" gate to apply.
|
|
378
325
|
const normalized = normalizeIncomingEvent({ text: enrichedText, metadata, ownAgentId });
|
|
379
|
-
if (!normalized.shouldProcess) {
|
|
380
|
-
const receiverId = metadata.receiver?.id ?? metadata.receiverId;
|
|
381
|
-
runtimeLog.warn('AgentHost', `skip reason=${normalized.reason || 'not_relevant'} ownAgentId=${ownAgentId} receiverId=${receiverId ?? 'null'} chatId=${normalized.chatId ?? 'null'}`);
|
|
382
|
-
return;
|
|
383
|
-
}
|
|
384
326
|
const sessionId = this.sessionResolver(normalized, ownAgentId);
|
|
385
327
|
if (!sessionId) {
|
|
386
328
|
runtimeLog.warn('AgentHost', `session resolver returned null ownAgentId=${ownAgentId} chatId=${normalized.chatId ?? 'null'}`);
|
|
@@ -390,38 +332,10 @@ export class AgentHost {
|
|
|
390
332
|
if (!this.sessions.has(laneKey)) {
|
|
391
333
|
this.sessions.set(laneKey, { sessionId, ownAgentId });
|
|
392
334
|
}
|
|
393
|
-
|
|
394
|
-
//
|
|
395
|
-
//
|
|
396
|
-
|
|
397
|
-
// it, re-running a finished pipeline (the cross-wake token burn). Mirrors
|
|
398
|
-
// catch-up's ack-on-delivery; newer messages carry a later ts and still
|
|
399
|
-
// deliver, so a genuinely new turn in the same chat is not lost. Best-effort:
|
|
400
|
-
// never block or fail the wake.
|
|
401
|
-
const msgTs = (metadata.createdAt ?? metadata.timestamp ?? metadata.sentTimestamp);
|
|
402
|
-
if (normalized.chatId && typeof msgTs === 'string' && msgTs) {
|
|
403
|
-
void this._ackChatWatermark(normalized.chatId, msgTs);
|
|
404
|
-
}
|
|
405
|
-
else if (normalized.chatId) {
|
|
406
|
-
// ZIG-860: this ack silently not firing is exactly how the re-delivery
|
|
407
|
-
// bug hid — a live message with no usable timestamp must be loud.
|
|
408
|
-
runtimeLog.warn('AgentHost', `live-ack skipped: no timestamp on message chatId=${normalized.chatId} messageId=${messageId ?? 'null'}`);
|
|
409
|
-
}
|
|
410
|
-
return enqueued;
|
|
411
|
-
}
|
|
412
|
-
/** Best-effort: advance the inbox watermark for a chat handled live (ZIG-832 P4). */
|
|
413
|
-
async _ackChatWatermark(chatId, upTo) {
|
|
414
|
-
try {
|
|
415
|
-
if (!this._inbox) {
|
|
416
|
-
this._inbox = new InboxClient(this.options.operatorKey, this.options.agentId);
|
|
417
|
-
}
|
|
418
|
-
await this._inbox.ack([{ kind: 'chat', id: chatId, upTo }]);
|
|
419
|
-
}
|
|
420
|
-
catch (e) {
|
|
421
|
-
// warn, not debug (ZIG-860): a failing live-ack means catch-up will
|
|
422
|
-
// re-deliver this chat's backlog after the next host eviction.
|
|
423
|
-
runtimeLog.warn('AgentHost', `live-ack chat ${chatId} failed: ${e?.message ?? e}`);
|
|
424
|
-
}
|
|
335
|
+
// The inbox loop owns ack (act-before-ack, delivery law); the host only
|
|
336
|
+
// routes the event onto its lane. No live-path watermark ack — there is no
|
|
337
|
+
// separate live/push channel to reconcile against anymore.
|
|
338
|
+
return this.eventQueue.enqueue(normalized.event, laneKey);
|
|
425
339
|
}
|
|
426
340
|
async _processEvent(event, laneKey) {
|
|
427
341
|
const session = this.sessions.get(laneKey);
|
|
@@ -540,16 +454,26 @@ export class AgentHost {
|
|
|
540
454
|
const agent = metadata.agent;
|
|
541
455
|
return this.options.agentId || self?.id || agent?.id || metadata.ziggsAgentId || null;
|
|
542
456
|
}
|
|
543
|
-
async connectAsync(
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
this.catchUpInFlight = null;
|
|
551
|
-
await this._ensureCatchUp();
|
|
457
|
+
async connectAsync(_timeout) {
|
|
458
|
+
// Bootstrap active assigned tasks once (HTTP pull → lane), then start the
|
|
459
|
+
// per-agent inbox long-poll loop — the agent's sole wake source. There is
|
|
460
|
+
// no socket to connect and no reconnect: a crash is just the loop's next
|
|
461
|
+
// iteration (the cursor is server-side). `_timeout` is accepted for
|
|
462
|
+
// backward-compatible call sites but no longer gates a handshake.
|
|
463
|
+
if (this.inboxLoopEnabled) {
|
|
552
464
|
await this._bootstrapAssignedActiveTasks();
|
|
465
|
+
const label = this.options.name || 'agent';
|
|
466
|
+
this.inboxLoop = runInboxLoop({
|
|
467
|
+
creds: {
|
|
468
|
+
operatorKey: this.options.operatorKey,
|
|
469
|
+
agentId: this.options.agentId,
|
|
470
|
+
},
|
|
471
|
+
ownAgentId: this.options.agentId,
|
|
472
|
+
seen: this.seenMessages,
|
|
473
|
+
label,
|
|
474
|
+
onMessage: (text, metadata) => this.handleMessage(text, metadata),
|
|
475
|
+
onResourceEvent: (event) => this.handleResourceEvent(event),
|
|
476
|
+
});
|
|
553
477
|
}
|
|
554
478
|
return this;
|
|
555
479
|
}
|
|
@@ -630,11 +554,11 @@ export class AgentHost {
|
|
|
630
554
|
chatId: full.originChatId,
|
|
631
555
|
}, laneKey);
|
|
632
556
|
}
|
|
633
|
-
disconnect() { this.stateTimers.clearAll(); this.
|
|
557
|
+
disconnect() { this.stateTimers.clearAll(); void this.inboxLoop?.stop(); this.inboxLoop = null; }
|
|
634
558
|
/** Waits for all in-flight ticks to complete. Call before process.exit() on SIGTERM. */
|
|
635
559
|
drain(timeoutMs = 30_000) { return this.eventQueue.waitForIdle(timeoutMs); }
|
|
636
560
|
registerTool(tool) { return this.toolManager.register(tool); }
|
|
637
|
-
isConnected() { return this.
|
|
561
|
+
isConnected() { return this.inboxLoop?.running ?? false; }
|
|
638
562
|
}
|
|
639
563
|
/**
|
|
640
564
|
* createAgent(config) → AgentHost
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
|
|
2
2
|
import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js';
|
|
3
3
|
import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js';
|
|
4
|
+
import { ConnectionsClient } from '@ziggs-ai/api-client';
|
|
4
5
|
import { AgentHost } from '../AgentHost.js';
|
|
5
6
|
import { thinkingDefaults } from '../runtime/defaults.js';
|
|
6
7
|
/**
|
|
@@ -368,21 +369,21 @@ export async function createMcpAgent(options) {
|
|
|
368
369
|
if (!transportConfig.serverUrl)
|
|
369
370
|
return null;
|
|
370
371
|
const operatorKey = transportConfig.operatorKey || process.env.ZIGGS_OPERATOR_KEY;
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
authorization: `Bearer ${operatorKey}`,
|
|
376
|
-
'x-agent-id': agentId,
|
|
377
|
-
'x-on-behalf-of-user': userId,
|
|
378
|
-
},
|
|
379
|
-
body: JSON.stringify({
|
|
372
|
+
if (!operatorKey)
|
|
373
|
+
return null;
|
|
374
|
+
try {
|
|
375
|
+
const body = await new ConnectionsClient(operatorKey, agentId, backendBase).requestMcpConnection({
|
|
380
376
|
serverUrl: transportConfig.serverUrl,
|
|
381
377
|
tools: tools.map(t => t.name),
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
378
|
+
onBehalfOfUserId: userId,
|
|
379
|
+
});
|
|
380
|
+
return (body?.request?.requestId) ?? null;
|
|
381
|
+
}
|
|
382
|
+
catch {
|
|
383
|
+
// Best-effort: the backend dedups pending requests and the caller falls
|
|
384
|
+
// back to a plain Connections-page link when there's no requestId.
|
|
385
|
+
return null;
|
|
386
|
+
}
|
|
386
387
|
};
|
|
387
388
|
const caller = async (name, args, context) => {
|
|
388
389
|
const userId = typeof context.senderId === 'string' ? context.senderId : undefined;
|
package/dist/agent/Agent.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { AgentMachine } from '../runtime/AgentMachine.js';
|
|
2
|
+
import { createWakeWriteKeys } from '../runtime/wakeWriteKeys.js';
|
|
2
3
|
import { runTurn } from '../runtime/runTurn.js';
|
|
3
4
|
import { PromptBuilder } from '../runtime/PromptBuilder.js';
|
|
4
5
|
/**
|
|
@@ -43,6 +44,9 @@ export class Agent {
|
|
|
43
44
|
memory,
|
|
44
45
|
input: { identity, ctx, state },
|
|
45
46
|
transitionTap,
|
|
47
|
+
// One key stream per wake, shared by every state this tick runs through —
|
|
48
|
+
// per-state streams would restart the ordinal and collide across states.
|
|
49
|
+
wakeKeys: createWakeWriteKeys(identity.laneKey, event),
|
|
46
50
|
});
|
|
47
51
|
// `send` awaits the recursive _step until the machine settles at a
|
|
48
52
|
// parked state (or stops). When it returns, we have the final state.
|
|
@@ -13,6 +13,10 @@ export interface ClaudeSdkAgentOptions {
|
|
|
13
13
|
operatorKey: string;
|
|
14
14
|
agentId: string;
|
|
15
15
|
maxTurns?: number;
|
|
16
|
+
/** ZIG-860: when true, never resume the prior Claude session — each wake is a
|
|
17
|
+
* fresh transcript. Keeps per-wake input flat on step-driven orchestrators
|
|
18
|
+
* whose wakes already carry their state. */
|
|
19
|
+
statelessPerWake?: boolean;
|
|
16
20
|
/** Injectable for tests. Defaults to @anthropic-ai/claude-agent-sdk query(). */
|
|
17
21
|
queryFn?: typeof query;
|
|
18
22
|
}
|
|
@@ -32,6 +36,7 @@ export declare class ClaudeSdkAgent {
|
|
|
32
36
|
private operatorKey;
|
|
33
37
|
private agentId;
|
|
34
38
|
private maxTurns;
|
|
39
|
+
private statelessPerWake;
|
|
35
40
|
private queryFn;
|
|
36
41
|
constructor(opts: ClaudeSdkAgentOptions);
|
|
37
42
|
tick({ event, ctx, state, identity, eff }: TickInput): Promise<TickOutput>;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { query } from '@anthropic-ai/claude-agent-sdk';
|
|
2
2
|
import { outcomeFromEvent } from '../runtime/buildOutcome.js';
|
|
3
3
|
import { bootstrapCtx } from '../runtime/bootstrapCtx.js';
|
|
4
|
+
import { createWakeWriteKeys } from '../runtime/wakeWriteKeys.js';
|
|
4
5
|
import { CLAUDE_SDK_PROTOCOL } from './claudeSdkProtocol.js';
|
|
5
6
|
import { buildZiggsSdkMcpServer } from './toolManagerToSdkMcp.js';
|
|
6
7
|
import { claudeQueryOptions, buildClaudeEventPrompt, summarizeContext, extractAssistantText, } from './claudeCognition.js';
|
|
@@ -24,9 +25,9 @@ const MAX_TASK_INPUT_TOKENS = Number(process.env.CLAUDE_SDK_MAX_INPUT_TOKENS ??
|
|
|
24
25
|
/**
|
|
25
26
|
* Per-lane wake-rate circuit breaker. The per-wake caps above bound one wake;
|
|
26
27
|
* nothing bounded the NUMBER of wakes — the agent-private artifact echo drove
|
|
27
|
-
* one wake per wake, forever. The echo is fixed at the source (backend
|
|
28
|
-
*
|
|
29
|
-
* future echo path: a lane waking faster than real traffic plausibly drives it
|
|
28
|
+
* one wake per wake, forever. The echo is fixed at the source (backend
|
|
29
|
+
* targeting never routes an agent its own writes), so this is a backstop for
|
|
30
|
+
* any future echo path: a lane waking faster than real traffic plausibly drives it
|
|
30
31
|
* skips the LLM (and logs loudly) instead of draining the fleet key. */
|
|
31
32
|
const WAKE_WINDOW_MS = 10 * 60_000;
|
|
32
33
|
const MAX_WAKES_PER_WINDOW = Number(process.env.CLAUDE_SDK_MAX_WAKES_PER_WINDOW ?? 40);
|
|
@@ -141,6 +142,7 @@ export class ClaudeSdkAgent {
|
|
|
141
142
|
operatorKey;
|
|
142
143
|
agentId;
|
|
143
144
|
maxTurns;
|
|
145
|
+
statelessPerWake;
|
|
144
146
|
queryFn;
|
|
145
147
|
constructor(opts) {
|
|
146
148
|
if (!opts.workflow?.states)
|
|
@@ -158,6 +160,7 @@ export class ClaudeSdkAgent {
|
|
|
158
160
|
this.operatorKey = opts.operatorKey;
|
|
159
161
|
this.agentId = opts.agentId;
|
|
160
162
|
this.maxTurns = opts.maxTurns ?? DEFAULT_MAX_TURNS;
|
|
163
|
+
this.statelessPerWake = opts.statelessPerWake ?? false;
|
|
161
164
|
this.queryFn = opts.queryFn ?? query;
|
|
162
165
|
}
|
|
163
166
|
async tick({ event, ctx, state, identity, eff }) {
|
|
@@ -214,6 +217,10 @@ export class ClaudeSdkAgent {
|
|
|
214
217
|
].filter(Boolean).join('\n');
|
|
215
218
|
const systemPrompt = `${CLAUDE_SDK_PROTOCOL}\n\n${roleBlock}`;
|
|
216
219
|
const runId = crypto.randomUUID();
|
|
220
|
+
// Replay-stable write keys for this wake (runId is random and can't serve):
|
|
221
|
+
// a crash-replay of the same event derives the same keys, so the backend
|
|
222
|
+
// drops the duplicate artifacts / no-ops the repeated task transitions.
|
|
223
|
+
const wakeKeys = createWakeWriteKeys(identity.laneKey, rawEvent);
|
|
217
224
|
const toolContext = {
|
|
218
225
|
operatorKey: this.operatorKey,
|
|
219
226
|
agentId: this.agentId,
|
|
@@ -228,6 +235,7 @@ export class ClaudeSdkAgent {
|
|
|
228
235
|
// not just task_result events — see resolveOnBehalfAgreementId.
|
|
229
236
|
onBehalfOfAgreementId: resolveOnBehalfAgreementId(rawEvent, contextSnap, this.agentId, identity.sessionId),
|
|
230
237
|
runId,
|
|
238
|
+
wakeKeys,
|
|
231
239
|
};
|
|
232
240
|
const availableTools = await this.toolManager.getAvailableTools();
|
|
233
241
|
const mcpServer = buildZiggsSdkMcpServer(availableTools, () => toolContext, {
|
|
@@ -248,7 +256,10 @@ export class ClaudeSdkAgent {
|
|
|
248
256
|
if (line)
|
|
249
257
|
runtimeLog.warn('ClaudeSdkAgent', `stderr session=${identity.sessionId}: ${line}`);
|
|
250
258
|
},
|
|
251
|
-
|
|
259
|
+
// ZIG-860: stateless-per-wake never resumes — each wake is a fresh
|
|
260
|
+
// transcript, so per-wake input stays flat instead of replaying the
|
|
261
|
+
// whole lane history every turn.
|
|
262
|
+
resume: this.statelessPerWake ? undefined : nextCtx.claudeSessionId,
|
|
252
263
|
});
|
|
253
264
|
let metrics = { llmCalls: 0, tokens: { total: 0, input: 0, output: 0, cacheRead: 0, cacheCreation: 0 } };
|
|
254
265
|
let sessionId = nextCtx.claudeSessionId;
|
|
@@ -274,7 +285,10 @@ export class ClaudeSdkAgent {
|
|
|
274
285
|
if (message.type === 'assistant') {
|
|
275
286
|
const text = extractAssistantText(message).trim();
|
|
276
287
|
if (text) {
|
|
277
|
-
|
|
288
|
+
// Ordinal drawn synchronously so the fire-and-forget below can't
|
|
289
|
+
// reorder key assignment against other writes of this wake.
|
|
290
|
+
const idempotencyKey = wakeKeys.artifactKey();
|
|
291
|
+
void eff({ kind: 'record-event', sessionId: identity.sessionId, entry: { kind: 'thought', text }, idempotencyKey })
|
|
278
292
|
.catch((err) => runtimeLog.warn('ClaudeSdkAgent', `record thought failed session=${identity.sessionId}: ${err.message}`));
|
|
279
293
|
}
|
|
280
294
|
}
|
|
@@ -298,7 +312,12 @@ export class ClaudeSdkAgent {
|
|
|
298
312
|
runtimeLog.warn('ClaudeSdkAgent', `query failed session=${identity.sessionId}: ${err.message}`);
|
|
299
313
|
throw err;
|
|
300
314
|
}
|
|
301
|
-
|
|
315
|
+
// Stateless-per-wake keeps no session id — clear any carried over from the
|
|
316
|
+
// prior ctx so the next wake starts fresh too (ZIG-860). Otherwise remember
|
|
317
|
+
// this wake's session for resume.
|
|
318
|
+
if (this.statelessPerWake)
|
|
319
|
+
nextCtx.claudeSessionId = undefined;
|
|
320
|
+
else if (sessionId)
|
|
302
321
|
nextCtx.claudeSessionId = sessionId;
|
|
303
322
|
// ZIG-816: bound resumed-transcript growth. `metrics.tokens.input` is the
|
|
304
323
|
// wake's billable input off the result message (dominated by the resumed
|
|
@@ -63,5 +63,12 @@ export declare function summarizeContext(snap: Record<string, unknown> | null |
|
|
|
63
63
|
* Turn a Ziggs wake event + context digest into the user prompt that starts a
|
|
64
64
|
* Claude-brain turn. The agent's standing instructions come from its system
|
|
65
65
|
* prompt (protocol + role); this is just "here's what woke you, go handle it."
|
|
66
|
+
*
|
|
67
|
+
* ZIG-880: EventQueue coalesces events that arrive while the lane is busy into
|
|
68
|
+
* a `{type:'batch', events:[...]}` frame. The FSM path unwraps those
|
|
69
|
+
* (PromptBuilder), but this path used to render just "Event type: batch" —
|
|
70
|
+
* every payload inside (including task results) was invisible to the model,
|
|
71
|
+
* stalling delegation pipelines whose notifications share one chat lane.
|
|
72
|
+
* Render each batched event in order instead.
|
|
66
73
|
*/
|
|
67
74
|
export declare function buildClaudeEventPrompt(event: RawEvent | undefined, contextSummary: string): string;
|