@ziggs-ai/agent-sdk 0.10.0 → 0.10.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +23 -0
- package/README.md +9 -1
- package/dist/AgentHost.d.ts +50 -14
- package/dist/AgentHost.js +129 -7
- package/dist/agent/Agent.js +2 -2
- package/dist/agent/ClaudeSdkAgent.d.ts +23 -1
- package/dist/agent/ClaudeSdkAgent.js +156 -84
- package/dist/agent/claudeCognition.d.ts +25 -3
- package/dist/agent/claudeCognition.js +33 -1
- package/dist/agent/claudeSdkTools.d.ts +2 -2
- package/dist/agent/claudeSdkTools.js +1 -1
- package/dist/agent/toolManagerToSdkMcp.js +1 -1
- package/dist/context/applyEffects.js +4 -3
- package/dist/index.d.ts +6 -4
- package/dist/index.js +7 -3
- 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/bootstrapCtx.js +6 -8
- package/dist/runtime/buildOutcome.d.ts +9 -1
- package/dist/runtime/buildOutcome.js +29 -6
- package/dist/runtime/{wakeWriteKeys.d.ts → deliveryWriteKeys.d.ts} +2 -2
- package/dist/runtime/{wakeWriteKeys.js → deliveryWriteKeys.js} +15 -22
- 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/runtime/taskIntake.js +6 -4
- package/dist/server/ConnectionPool.d.ts +11 -53
- package/dist/server/ConnectionPool.js +29 -60
- package/dist/server/EventQueue.d.ts +6 -0
- package/dist/server/EventQueue.js +21 -6
- package/dist/server/SeenMessages.d.ts +7 -0
- package/dist/server/SeenMessages.js +15 -0
- package/dist/server/ZiggsEffectHandler.d.ts +1 -0
- package/dist/server/ZiggsEffectHandler.js +18 -11
- package/dist/server/createHealthServer.js +1 -1
- package/dist/server/inboxLoop.d.ts +17 -2
- package/dist/server/inboxLoop.js +92 -16
- package/dist/server/polls/PollKind.d.ts +72 -0
- package/dist/server/polls/PollKind.js +53 -0
- package/dist/server/polls/PollRunner.d.ts +62 -0
- package/dist/server/polls/PollRunner.js +138 -0
- package/dist/server/questMatch.d.ts +5 -0
- package/dist/server/questMatch.js +14 -0
- package/dist/server/runLauncher.d.ts +0 -6
- package/dist/server/runLauncher.js +54 -27
- package/dist/server/tools/tier1/protocolRunner.d.ts +4 -2
- package/dist/server/tools/tier1/protocolRunner.js +30 -34
- package/dist/server/tools/tier1/protocolTools.js +23 -13
- package/dist/server/tools/tier2/connectionTools.js +12 -1
- package/dist/tasks/protocolRegistry.d.ts +27 -0
- package/dist/tasks/protocolRegistry.js +36 -0
- package/dist/types.d.ts +4 -13
- package/package.json +2 -2
- package/dist/server/staleBacklogGate.d.ts +0 -57
- package/dist/server/staleBacklogGate.js +0 -100
|
@@ -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 */ }
|
|
@@ -18,14 +18,15 @@ import { migrateLegacyTaskFields } from './taskBookkeeping.js';
|
|
|
18
18
|
export function bootstrapCtx(identity, existing) {
|
|
19
19
|
if (existing) {
|
|
20
20
|
const legacy = existing;
|
|
21
|
+
// ZIG-1128: the lane-level mirrors are destructured OFF, not carried through.
|
|
22
|
+
// `migrateLegacyTaskFields` has already folded whatever they held into
|
|
23
|
+
// `tasks`; keeping the empty husks on the restored Ctx is what let a live FSM
|
|
24
|
+
// guard read one and see "nothing delegated" forever.
|
|
25
|
+
const { pendingSubtasks: _pendingSubtasks, collectedResults: _collectedResults, delegatedAgreementIds: _delegatedAgreementIds, ...rest } = existing;
|
|
21
26
|
return {
|
|
22
|
-
...
|
|
27
|
+
...rest,
|
|
23
28
|
identity,
|
|
24
29
|
tasks: migrateLegacyTaskFields(legacy),
|
|
25
|
-
// ZIG-1057: deprecated mirrors stay empty; use all*() readers.
|
|
26
|
-
pendingSubtasks: {},
|
|
27
|
-
collectedResults: [],
|
|
28
|
-
delegatedAgreementIds: [],
|
|
29
30
|
};
|
|
30
31
|
}
|
|
31
32
|
return {
|
|
@@ -34,9 +35,6 @@ export function bootstrapCtx(identity, existing) {
|
|
|
34
35
|
activeAgreementId: null,
|
|
35
36
|
activeTaskId: null,
|
|
36
37
|
pendingProposalAgreementId: null,
|
|
37
|
-
pendingSubtasks: {},
|
|
38
|
-
collectedResults: [],
|
|
39
|
-
delegatedAgreementIds: [],
|
|
40
38
|
toolResults: [],
|
|
41
39
|
lastOutcome: { kind: 'enter' },
|
|
42
40
|
};
|
|
@@ -3,7 +3,6 @@ import type { Action, AgreementRef, Ctx, Outcome, TaskRef, ToolResultEntry } fro
|
|
|
3
3
|
export interface WireResult {
|
|
4
4
|
taskId?: string;
|
|
5
5
|
state?: string;
|
|
6
|
-
status?: string;
|
|
7
6
|
agreement?: AgreementRef;
|
|
8
7
|
agreementId?: string;
|
|
9
8
|
proposal?: {
|
|
@@ -37,6 +36,15 @@ export interface EmittedEvent {
|
|
|
37
36
|
senderId?: string;
|
|
38
37
|
[key: string]: unknown;
|
|
39
38
|
}
|
|
39
|
+
/**
|
|
40
|
+
* A chat message whose stated receiver is a DIFFERENT party. `receiverId` is
|
|
41
|
+
* the sender's addressing decision, carried on every wire frame; the
|
|
42
|
+
* human-broadcast sentinel counts as "elsewhere" for an agent (a broadcast is
|
|
43
|
+
* addressed to the room's humans). A frame with NO receiver keeps the turn —
|
|
44
|
+
* we cannot know it is not ours, and skipping on absence would deafen agents
|
|
45
|
+
* to malformed or legacy frames.
|
|
46
|
+
*/
|
|
47
|
+
export declare function isMessageAddressedElsewhere(ev: RawEvent | null | undefined, ownAgentId: string | null | undefined): boolean;
|
|
40
48
|
/**
|
|
41
49
|
* Converts a raw incoming event into the single Outcome that re-enters the
|
|
42
50
|
* machine.
|
|
@@ -1,6 +1,21 @@
|
|
|
1
1
|
import { isProtocolToolName, mapProtocolToolToOperation } from '../tasks/protocolRegistry.js';
|
|
2
2
|
import { getBatchEvents, isTaskResultRelevantToAgent } from '../context/batch.js';
|
|
3
3
|
import { bookForDelegation, ensureTaskBook, findBookHoldingPending, removeTaskBook, } from './taskBookkeeping.js';
|
|
4
|
+
/**
|
|
5
|
+
* A chat message whose stated receiver is a DIFFERENT party. `receiverId` is
|
|
6
|
+
* the sender's addressing decision, carried on every wire frame; the
|
|
7
|
+
* human-broadcast sentinel counts as "elsewhere" for an agent (a broadcast is
|
|
8
|
+
* addressed to the room's humans). A frame with NO receiver keeps the turn —
|
|
9
|
+
* we cannot know it is not ours, and skipping on absence would deafen agents
|
|
10
|
+
* to malformed or legacy frames.
|
|
11
|
+
*/
|
|
12
|
+
export function isMessageAddressedElsewhere(ev, ownAgentId) {
|
|
13
|
+
return Boolean(ev &&
|
|
14
|
+
ev.type === 'message' &&
|
|
15
|
+
ownAgentId &&
|
|
16
|
+
ev.receiverId &&
|
|
17
|
+
ev.receiverId !== ownAgentId);
|
|
18
|
+
}
|
|
4
19
|
// ── outcomeFromEvent: incoming wire event → Outcome ──────────────────────────
|
|
5
20
|
/**
|
|
6
21
|
* Converts a raw incoming event into the single Outcome that re-enters the
|
|
@@ -139,6 +154,14 @@ function singleEventToOutcome(ev, ownAgentId) {
|
|
|
139
154
|
// Suppress our own messages echoed back through the chat fanout.
|
|
140
155
|
if (ownAgentId && ev.senderId && ev.senderId === ownAgentId)
|
|
141
156
|
return null;
|
|
157
|
+
// Attention gate: a message addressed to someone else is room context, not
|
|
158
|
+
// a turn. Delivery is audience-based on purpose (everyone in the chat hears
|
|
159
|
+
// everything), so in a multi-agent room every agent used to burn an LLM turn
|
|
160
|
+
// per utterance just to decide it wasn't theirs — the sender already decided
|
|
161
|
+
// that by naming a receiver. The message still lands in chat history and
|
|
162
|
+
// rides in on the next wake's read-back; nothing is lost but the interrupt.
|
|
163
|
+
if (isMessageAddressedElsewhere(ev, ownAgentId))
|
|
164
|
+
return null;
|
|
142
165
|
const text = typeof ev.text === 'string' ? ev.text : undefined;
|
|
143
166
|
const senderType = typeof ev.senderType === 'string' ? ev.senderType : undefined;
|
|
144
167
|
return { kind: 'message-received', text, senderId: ev.senderId, senderType };
|
|
@@ -286,8 +309,9 @@ function inferKindFromProtocolOp(op, result) {
|
|
|
286
309
|
return 'task-delegated';
|
|
287
310
|
}
|
|
288
311
|
if (op === 'task-update') {
|
|
289
|
-
|
|
290
|
-
|
|
312
|
+
// Same resolution as `isStateChangingProtocolEvent` above — `state` only.
|
|
313
|
+
const state = task?.state || r.state;
|
|
314
|
+
if (state === 'completed' || state === 'failed')
|
|
291
315
|
return 'task-closed';
|
|
292
316
|
return 'tool-result';
|
|
293
317
|
}
|
|
@@ -462,10 +486,9 @@ export function applyOutcomeToCtx(ctx, outcome) {
|
|
|
462
486
|
}
|
|
463
487
|
case 'task-closed': {
|
|
464
488
|
// Clear only this task's entry — sibling open tasks keep their books (ZIG-977).
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
ctx.activeTaskId;
|
|
489
|
+
// ZIG-1171: buildOutcomeOfKind already collapses result.task.taskId /
|
|
490
|
+
// result.taskId into outcome.taskId — do not re-walk the raw result here.
|
|
491
|
+
const closedId = outcome.taskId || ctx.activeTaskId;
|
|
469
492
|
if (closedId)
|
|
470
493
|
removeTaskBook(ctx, closedId);
|
|
471
494
|
else {
|
|
@@ -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,10 +93,17 @@ 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;
|
|
100
|
+
// ZIG-1171: one content-hash builder; tags differ, the shape does not.
|
|
101
|
+
const keyed = (tag, material, opts) => {
|
|
102
|
+
if (wakeHash === null)
|
|
103
|
+
return undefined;
|
|
104
|
+
const body = `wk_${wakeHash}_${tag}_${sha(material).slice(0, 16)}`;
|
|
105
|
+
return opts?.asMessageId ? `sdk_idem_${body}` : body;
|
|
106
|
+
};
|
|
100
107
|
return {
|
|
101
108
|
wakeKey: wakeHash === null ? null : `wk_${wakeHash}`,
|
|
102
109
|
artifactKey(content) {
|
|
@@ -110,37 +117,23 @@ export function createWakeWriteKeys(laneKey, event) {
|
|
|
110
117
|
// agent like agent-definer. Callers without content keep the ordinal.
|
|
111
118
|
if (content === undefined)
|
|
112
119
|
return `wk_${wakeHash}_a${ordinal++}`;
|
|
113
|
-
const
|
|
114
|
-
return
|
|
120
|
+
const material = typeof content === 'string' ? content : JSON.stringify(content) ?? '';
|
|
121
|
+
return keyed('a', material);
|
|
115
122
|
},
|
|
116
123
|
taskStateKey(taskId, state) {
|
|
117
|
-
|
|
118
|
-
return undefined;
|
|
119
|
-
return `wk_${wakeHash}_t_${sha(`${taskId}\n${state}`).slice(0, 16)}`;
|
|
124
|
+
return keyed('t', `${taskId}\n${state}`);
|
|
120
125
|
},
|
|
121
126
|
messageKey(fields) {
|
|
122
|
-
|
|
123
|
-
return undefined;
|
|
124
|
-
const msgHash = sha(`${fields.sessionId ?? ''}\n${fields.receiverId ?? ''}\n${fields.text}`).slice(0, 16);
|
|
125
|
-
return `sdk_idem_wk_${wakeHash}_m_${msgHash}`;
|
|
127
|
+
return keyed('m', `${fields.sessionId ?? ''}\n${fields.receiverId ?? ''}\n${fields.text}`, { asMessageId: true });
|
|
126
128
|
},
|
|
127
129
|
taskSpawnKey(fields) {
|
|
128
|
-
|
|
129
|
-
return undefined;
|
|
130
|
-
const spawnHash = sha(`${fields.agreementId}\n${fields.description}\n${fields.assigneeId ?? ''}\n${fields.parentTaskId ?? ''}`).slice(0, 16);
|
|
131
|
-
return `wk_${wakeHash}_s_${spawnHash}`;
|
|
130
|
+
return keyed('s', `${fields.agreementId}\n${fields.description}\n${fields.assigneeId ?? ''}\n${fields.parentTaskId ?? ''}`);
|
|
132
131
|
},
|
|
133
132
|
agreementProposeKey(fields) {
|
|
134
|
-
|
|
135
|
-
return undefined;
|
|
136
|
-
const proposeHash = sha(`${fields.proposedTo}\n${fields.providerId ?? ''}\n${fields.description}\n${fields.parentAgreementId ?? ''}`).slice(0, 16);
|
|
137
|
-
return `wk_${wakeHash}_ap_${proposeHash}`;
|
|
133
|
+
return keyed('ap', `${fields.proposedTo}\n${fields.providerId ?? ''}\n${fields.description}\n${fields.parentAgreementId ?? ''}`);
|
|
138
134
|
},
|
|
139
135
|
agreementSubcontractKey(fields) {
|
|
140
|
-
|
|
141
|
-
return undefined;
|
|
142
|
-
const subHash = sha(`${fields.executorId}\n${fields.description}\n${fields.parentAgreementId}`).slice(0, 16);
|
|
143
|
-
return `wk_${wakeHash}_asc_${subHash}`;
|
|
136
|
+
return keyed('asc', `${fields.executorId}\n${fields.description}\n${fields.parentAgreementId}`);
|
|
144
137
|
},
|
|
145
138
|
};
|
|
146
139
|
}
|
|
@@ -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,
|
|
@@ -49,9 +49,10 @@ export function buildTaskIntakeState(initial) {
|
|
|
49
49
|
// Function form so a failed task_set_result surfaces as an error outcome
|
|
50
50
|
// (→ dflt error transition) instead of a phantom task-closed.
|
|
51
51
|
produces: (result) => {
|
|
52
|
+
// `state` only — ZIG-1057 cut `status` from the wire (ZIG-1128).
|
|
52
53
|
const r = result;
|
|
53
|
-
const
|
|
54
|
-
return
|
|
54
|
+
const state = r?.task?.state ?? r?.state;
|
|
55
|
+
return state === 'completed' || state === 'failed' ? 'task-closed' : 'error';
|
|
55
56
|
},
|
|
56
57
|
terminatesLoop: true,
|
|
57
58
|
},
|
|
@@ -63,9 +64,10 @@ export function buildTaskIntakeState(initial) {
|
|
|
63
64
|
},
|
|
64
65
|
tool: 'task_set_result',
|
|
65
66
|
produces: (result) => {
|
|
67
|
+
// `state` only — ZIG-1057 cut `status` from the wire (ZIG-1128).
|
|
66
68
|
const r = result;
|
|
67
|
-
const
|
|
68
|
-
return
|
|
69
|
+
const state = r?.task?.state ?? r?.state;
|
|
70
|
+
return state === 'completed' || state === 'failed' ? 'task-closed' : 'error';
|
|
69
71
|
},
|
|
70
72
|
terminatesLoop: true,
|
|
71
73
|
},
|
|
@@ -1,17 +1,3 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* ConnectionPool — multi-agent fleet manager.
|
|
3
|
-
*
|
|
4
|
-
* Holds N `AgentHostOptions` configs but only constructs/wakes an
|
|
5
|
-
* `AgentHost` (one WS connection) when needed. Idle hosts are slept after
|
|
6
|
-
* `idleTimeoutMs`. At most `maxActive` hosts are connected at a time; LRU
|
|
7
|
-
* eviction drops the oldest if the cap is reached. With `startControl`, a
|
|
8
|
-
* separate "launcher" socket receives backend-driven wake hints so the pool
|
|
9
|
-
* spins up the right host on demand.
|
|
10
|
-
*
|
|
11
|
-
* This is how a single Node process serves a fleet of agents without
|
|
12
|
-
* holding 1000 open sockets — typical config: 1000+ registered, ~50 live.
|
|
13
|
-
*/
|
|
14
|
-
import { type InboxOperatorAgentEntry, type StartAgentOptions } from '@ziggs-ai/api-client';
|
|
15
1
|
import { AgentHost, type AgentHostOptions } from '../AgentHost.js';
|
|
16
2
|
interface ConnectionManagerMeta {
|
|
17
3
|
domain?: string;
|
|
@@ -24,44 +10,27 @@ interface QueryFilter {
|
|
|
24
10
|
expertise?: string[];
|
|
25
11
|
tags?: string[];
|
|
26
12
|
}
|
|
27
|
-
export interface PoolOptions {
|
|
28
|
-
maxActive?: number;
|
|
29
|
-
idleTimeoutMs?: number;
|
|
30
|
-
/**
|
|
31
|
-
* Gate on the operator-poll lazy start (ZIG-910): return false to leave an
|
|
32
|
-
* actionable agent's host down this pass. Explicit startAgent()s are not gated.
|
|
33
|
-
*/
|
|
34
|
-
shouldLazyStart?: (entry: InboxOperatorAgentEntry) => boolean;
|
|
35
|
-
}
|
|
36
13
|
export interface SendToOptions {
|
|
37
14
|
timeoutMs?: number;
|
|
38
15
|
}
|
|
39
|
-
export interface ControlOptions {
|
|
40
|
-
wsUrl?: string;
|
|
41
|
-
operatorKey?: string;
|
|
42
|
-
}
|
|
43
16
|
export declare class ConnectionPool {
|
|
44
17
|
private readonly manager;
|
|
45
18
|
private readonly configs;
|
|
46
19
|
private readonly responseListeners;
|
|
47
|
-
constructor({ maxActive, idleTimeoutMs, shouldLazyStart }?: PoolOptions);
|
|
48
20
|
/**
|
|
49
|
-
* Register N agent configs.
|
|
50
|
-
*
|
|
21
|
+
* Register N agent configs. Nothing connects until `connectAll()`; a config
|
|
22
|
+
* registered and never connected is an agent nobody hosts, which is the
|
|
23
|
+
* failure this pool exists to make impossible.
|
|
51
24
|
*/
|
|
52
25
|
register(configs: AgentHostOptions[], metaArr?: ConnectionManagerMeta[]): void;
|
|
53
|
-
/**
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
startAgent(agentId: string, opts?: StartAgentOptions): Promise<AgentHost>;
|
|
59
|
-
/** Agents kept up explicitly (launcher `preStart`), not by demand. */
|
|
60
|
-
listPinned(): string[];
|
|
61
|
-
sleep(agentId: string): Promise<void>;
|
|
26
|
+
/** Connect every registered agent. Reports the ones that could not come up. */
|
|
27
|
+
connectAll(): Promise<void>;
|
|
28
|
+
/** The host for one agent, connecting it if it somehow is not up yet. */
|
|
29
|
+
startAgent(agentId: string): Promise<AgentHost>;
|
|
30
|
+
stopAgent(agentId: string): Promise<void>;
|
|
62
31
|
disconnectAll(): Promise<void>;
|
|
63
32
|
/**
|
|
64
|
-
*
|
|
33
|
+
* Deliver a message to the named agent and wait for its first outbound
|
|
65
34
|
* `send-message` effect — i.e. the agent's reply. Returns the reply text.
|
|
66
35
|
*
|
|
67
36
|
* The effectTap injected in `register` drains a per-agent listener queue;
|
|
@@ -70,28 +39,17 @@ export declare class ConnectionPool {
|
|
|
70
39
|
*/
|
|
71
40
|
requestFrom(agentId: string, text: string, metadata?: Record<string, unknown>, { timeoutMs }?: SendToOptions): Promise<string>;
|
|
72
41
|
/**
|
|
73
|
-
*
|
|
74
|
-
*
|
|
42
|
+
* Deliver one wire frame to the named agent. Returns once the host has
|
|
43
|
+
* finished processing.
|
|
75
44
|
*
|
|
76
45
|
* Note: does not return the agent's response. For request/response
|
|
77
46
|
* semantics use `requestFrom`.
|
|
78
47
|
*/
|
|
79
48
|
sendTo(agentId: string, text: string, metadata?: Record<string, unknown>, { timeoutMs }?: SendToOptions): Promise<null>;
|
|
80
|
-
/**
|
|
81
|
-
* Start the fleet lazy-start loop (delivery law, phase 4): the manager
|
|
82
|
-
* long-polls `GET /inbox/operator` with this operator key and starts the host
|
|
83
|
-
* for any owned agent whose slice has actionable content. Each started host
|
|
84
|
-
* then consumes via its own per-agent inbox loop — this is ordinary lazy
|
|
85
|
-
* process management, not a pushed signal. `wsUrl` is accepted for
|
|
86
|
-
* back-compat but unused (the poll is HTTP).
|
|
87
|
-
*/
|
|
88
|
-
startControl({ operatorKey }?: ControlOptions): void;
|
|
89
|
-
stopControl(): void;
|
|
90
49
|
query(filter: QueryFilter): string[];
|
|
91
50
|
list(): string[];
|
|
92
51
|
listActive(): string[];
|
|
93
52
|
get size(): number;
|
|
94
|
-
get maxActive(): number;
|
|
95
53
|
getMeta(id: string): ConnectionManagerMeta | undefined;
|
|
96
54
|
}
|
|
97
55
|
export {};
|
|
@@ -1,30 +1,30 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* ConnectionPool —
|
|
2
|
+
* ConnectionPool — the agents this launcher process hosts.
|
|
3
3
|
*
|
|
4
|
-
* Holds N `AgentHostOptions` configs
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
* separate "launcher" socket receives backend-driven wake hints so the pool
|
|
9
|
-
* spins up the right host on demand.
|
|
4
|
+
* Holds N `AgentHostOptions` configs and connects every one of them at boot
|
|
5
|
+
* (ZIG-1108). Each host long-polls its own inbox for the life of the process —
|
|
6
|
+
* there is no agent socket. There is no subset, no cap and no lifecycle policy:
|
|
7
|
+
* a write releasing that poll is the only signal anything needs.
|
|
10
8
|
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
9
|
+
* What this replaced: a launcher that registered 1000+ configs and connected
|
|
10
|
+
* ~50, deciding which by long-polling `GET /inbox/operator` for a fleet-wide
|
|
11
|
+
* sweep, sleeping quiet hosts on an idle timer, evicting past `maxActive` by
|
|
12
|
+
* LRU, and exempting operator-named agents with a `pinned` flag. Whether an
|
|
13
|
+
* agent was alive came down to a Terraform string, and nothing noticed when the
|
|
14
|
+
* answer was "none of them".
|
|
13
15
|
*/
|
|
14
16
|
import { ConnectionManager } from '@ziggs-ai/api-client';
|
|
15
17
|
import { AgentHost } from '../AgentHost.js';
|
|
16
18
|
import { runtimeLog } from '../shared/runtimeLog.js';
|
|
17
19
|
export class ConnectionPool {
|
|
18
|
-
manager;
|
|
20
|
+
manager = new ConnectionManager();
|
|
19
21
|
configs = new Map();
|
|
20
22
|
// Listeners enqueued by requestFrom — drained on outbound send-message effects.
|
|
21
23
|
responseListeners = new Map();
|
|
22
|
-
constructor({ maxActive = 50, idleTimeoutMs = 60_000, shouldLazyStart } = {}) {
|
|
23
|
-
this.manager = new ConnectionManager({ maxActive, idleTimeoutMs, shouldLazyStart });
|
|
24
|
-
}
|
|
25
24
|
/**
|
|
26
|
-
* Register N agent configs.
|
|
27
|
-
*
|
|
25
|
+
* Register N agent configs. Nothing connects until `connectAll()`; a config
|
|
26
|
+
* registered and never connected is an agent nobody hosts, which is the
|
|
27
|
+
* failure this pool exists to make impossible.
|
|
28
28
|
*/
|
|
29
29
|
register(configs, metaArr = []) {
|
|
30
30
|
const metaByAgentId = new Map();
|
|
@@ -46,14 +46,7 @@ export class ConnectionPool {
|
|
|
46
46
|
const pool = this;
|
|
47
47
|
const host = new AgentHost({
|
|
48
48
|
...cfg,
|
|
49
|
-
// Inbox activity and in-flight effects both reset the idle timer:
|
|
50
|
-
// previously only pool.sendTo() touched it, so a host woken by its
|
|
51
|
-
// own inbox loop was torn down every idleTimeoutMs (default 60s)
|
|
52
|
-
// regardless of the work it was doing — losing lane ctx, Claude
|
|
53
|
-
// session ids, and armed timers each time.
|
|
54
|
-
onActivity: () => pool.manager.touch(agentId),
|
|
55
49
|
effectTap(effect, response) {
|
|
56
|
-
pool.manager.touch(agentId);
|
|
57
50
|
if (effect.kind === 'send-message') {
|
|
58
51
|
const listeners = pool.responseListeners.get(agentId);
|
|
59
52
|
if (listeners?.length)
|
|
@@ -63,34 +56,30 @@ export class ConnectionPool {
|
|
|
63
56
|
},
|
|
64
57
|
});
|
|
65
58
|
await host.connectAsync();
|
|
66
|
-
runtimeLog.debug('ConnectionPool', `
|
|
59
|
+
runtimeLog.debug('ConnectionPool', `connected "${agentId}"`);
|
|
67
60
|
return host;
|
|
68
61
|
}, async (host) => {
|
|
69
62
|
host.disconnect();
|
|
70
|
-
runtimeLog.debug('ConnectionPool', `
|
|
63
|
+
runtimeLog.debug('ConnectionPool', `disconnected "${agentId}"`);
|
|
71
64
|
}, metaByAgentId.get(agentId));
|
|
72
65
|
}
|
|
73
66
|
}
|
|
74
|
-
/**
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
* named rather than ones work happened to reach.
|
|
78
|
-
*/
|
|
79
|
-
async startAgent(agentId, opts = {}) {
|
|
80
|
-
return await this.manager.startAgent(agentId, opts);
|
|
67
|
+
/** Connect every registered agent. Reports the ones that could not come up. */
|
|
68
|
+
async connectAll() {
|
|
69
|
+
return this.manager.connectAll();
|
|
81
70
|
}
|
|
82
|
-
/**
|
|
83
|
-
|
|
84
|
-
return this.manager.
|
|
71
|
+
/** The host for one agent, connecting it if it somehow is not up yet. */
|
|
72
|
+
async startAgent(agentId) {
|
|
73
|
+
return await this.manager.startAgent(agentId);
|
|
85
74
|
}
|
|
86
|
-
async
|
|
87
|
-
return this.manager.
|
|
75
|
+
async stopAgent(agentId) {
|
|
76
|
+
return this.manager.stopAgent(agentId);
|
|
88
77
|
}
|
|
89
78
|
async disconnectAll() {
|
|
90
|
-
return this.manager.
|
|
79
|
+
return this.manager.stopAll();
|
|
91
80
|
}
|
|
92
81
|
/**
|
|
93
|
-
*
|
|
82
|
+
* Deliver a message to the named agent and wait for its first outbound
|
|
94
83
|
* `send-message` effect — i.e. the agent's reply. Returns the reply text.
|
|
95
84
|
*
|
|
96
85
|
* The effectTap injected in `register` drains a per-agent listener queue;
|
|
@@ -139,8 +128,8 @@ export class ConnectionPool {
|
|
|
139
128
|
});
|
|
140
129
|
}
|
|
141
130
|
/**
|
|
142
|
-
*
|
|
143
|
-
*
|
|
131
|
+
* Deliver one wire frame to the named agent. Returns once the host has
|
|
132
|
+
* finished processing.
|
|
144
133
|
*
|
|
145
134
|
* Note: does not return the agent's response. For request/response
|
|
146
135
|
* semantics use `requestFrom`.
|
|
@@ -149,31 +138,11 @@ export class ConnectionPool {
|
|
|
149
138
|
const host = await this.startAgent(agentId);
|
|
150
139
|
const timeoutPromise = new Promise((_, reject) => setTimeout(() => reject(new Error(`sendTo timeout for "${agentId}" after ${timeoutMs}ms`)), timeoutMs));
|
|
151
140
|
await Promise.race([host.handleMessage(text, metadata), timeoutPromise]);
|
|
152
|
-
this.manager.touch(agentId);
|
|
153
141
|
return null;
|
|
154
142
|
}
|
|
155
|
-
/**
|
|
156
|
-
* Start the fleet lazy-start loop (delivery law, phase 4): the manager
|
|
157
|
-
* long-polls `GET /inbox/operator` with this operator key and starts the host
|
|
158
|
-
* for any owned agent whose slice has actionable content. Each started host
|
|
159
|
-
* then consumes via its own per-agent inbox loop — this is ordinary lazy
|
|
160
|
-
* process management, not a pushed signal. `wsUrl` is accepted for
|
|
161
|
-
* back-compat but unused (the poll is HTTP).
|
|
162
|
-
*/
|
|
163
|
-
startControl({ operatorKey } = {}) {
|
|
164
|
-
if (!operatorKey) {
|
|
165
|
-
runtimeLog.warn('ConnectionPool', 'startControl: operatorKey is required — skipping');
|
|
166
|
-
return;
|
|
167
|
-
}
|
|
168
|
-
this.manager.start(operatorKey);
|
|
169
|
-
}
|
|
170
|
-
stopControl() {
|
|
171
|
-
void this.manager.stop();
|
|
172
|
-
}
|
|
173
143
|
query(filter) { return this.manager.query(filter); }
|
|
174
144
|
list() { return this.manager.list(); }
|
|
175
145
|
listActive() { return this.manager.listActive(); }
|
|
176
146
|
get size() { return this.manager.size; }
|
|
177
|
-
get maxActive() { return this.manager.maxActive; }
|
|
178
147
|
getMeta(id) { return this.manager.getMeta(id); }
|
|
179
148
|
}
|
|
@@ -7,6 +7,12 @@ export interface EventQueueOptions {
|
|
|
7
7
|
* batch dispatch. The runtime sees at most one in-flight processEvent call
|
|
8
8
|
* per laneKey at a time; multiple events arriving while busy are merged
|
|
9
9
|
* into a single { type: 'batch', events: [...] } frame on the next pass.
|
|
10
|
+
*
|
|
11
|
+
* ZIG-1191: `enqueue` REJECTS when the dispatch failed. It used to log the error
|
|
12
|
+
* and resolve, which made every caller — including the inbox loop, whose next
|
|
13
|
+
* statement is the ack — unable to tell a handled event from a lost one. A
|
|
14
|
+
* caller that has nothing to do with a failure should say so with `.catch()`;
|
|
15
|
+
* the one holding a delivery must not have that decision made for it.
|
|
10
16
|
*/
|
|
11
17
|
export declare class EventQueue {
|
|
12
18
|
private _processEvent;
|