@ziggs-ai/agent-sdk 0.2.2 → 0.4.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/CHANGELOG.md +18 -0
- package/README.md +1 -1
- package/dist/AgentHost.d.ts +12 -20
- package/dist/AgentHost.js +68 -142
- package/dist/agent/Agent.js +4 -0
- package/dist/agent/ClaudeSdkAgent.js +13 -4
- 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 +9 -4
- package/dist/agent/toolManagerToSdkMcp.js +1 -0
- package/dist/index.d.ts +5 -4
- package/dist/index.js +4 -2
- package/dist/ingress/normalizeIncoming.d.ts +23 -8
- package/dist/ingress/normalizeIncoming.js +21 -32
- package/dist/runtime/AgentMachine.d.ts +5 -2
- package/dist/runtime/AgentMachine.js +8 -2
- package/dist/runtime/PromptBuilder.js +2 -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 +27 -9
- package/dist/runtime/wakeWriteKeys.d.ts +25 -0
- package/dist/runtime/wakeWriteKeys.js +134 -0
- package/dist/server/ConnectionPool.d.ts +31 -9
- package/dist/server/ConnectionPool.js +21 -24
- package/dist/server/ZiggsEffectHandler.js +17 -12
- package/dist/server/createHealthServer.js +5 -5
- package/dist/server/inboxLoop.d.ts +50 -0
- package/dist/server/{InboxCatchUp.js → inboxLoop.js} +64 -81
- package/dist/server/runLauncher.d.ts +1 -1
- package/dist/server/runLauncher.js +20 -7
- package/dist/server/staleBacklogGate.d.ts +53 -0
- package/dist/server/staleBacklogGate.js +120 -0
- package/dist/server/tasks/TaskService.d.ts +2 -1
- package/dist/server/tasks/TaskService.js +5 -2
- 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/contextTools.js +1 -1
- package/dist/shared/operatorKeyClaims.d.ts +22 -0
- package/dist/shared/operatorKeyClaims.js +35 -0
- package/dist/types.d.ts +40 -0
- package/package.json +2 -2
- package/dist/server/InboxCatchUp.d.ts +0 -24
package/dist/index.d.ts
CHANGED
|
@@ -34,11 +34,14 @@ export { KICKOFF_STATE, injectKickoffDefault, buildKickoffState } from './runtim
|
|
|
34
34
|
export { validateWorkflow, WorkflowValidationError } from './runtime/validateWorkflow.js';
|
|
35
35
|
export { deriveRespondsToProposals, statesRespondToProposals, } from './runtime/deriveRespondsToProposals.js';
|
|
36
36
|
export { outcomeFromEvent, outcomeFromActionResult, applyOutcomeToCtx } from './runtime/buildOutcome.js';
|
|
37
|
-
export
|
|
37
|
+
export { createWakeWriteKeys } from './runtime/wakeWriteKeys.js';
|
|
38
|
+
export type { Outcome, OutcomeKind, Ctx, Identity, HistoryEntry, ToolResultEntry, Workflow, State, ParkedState, ThinkingState, MechanicalState, Action, Transition, PromptDef, ActionPrompt, Produces, ProducesFn, AgreementRef, TaskRef, ProposalRef, AgentConfig, DefineAgentInput, ServicesConfig, Effect, EffectKind, EffectResult, EffectResultMap, EffectHandler, LlmMessage, LlmToolCall, LlmToolSchema, LlmResponse, ContextSnapshot, ReadContextOpts, ToolCallResult, RecordedEntry, WakeWriteKeys, } from './types.js';
|
|
38
39
|
export { default } from './AgentHost.js';
|
|
39
40
|
export { validateContext, ContextWarning } from './cognition/validateContext.js';
|
|
40
41
|
export { runLauncher } from './server/runLauncher.js';
|
|
41
42
|
export { ConnectionPool } from './server/ConnectionPool.js';
|
|
43
|
+
export { createStaleBacklogGate, resolveReplayPolicy, newestActionableAt, parseReplayMaxAge, } from './server/staleBacklogGate.js';
|
|
44
|
+
export { decodeOperatorKeyClaims, boundKeyFleetWarning } from './shared/operatorKeyClaims.js';
|
|
42
45
|
export { createHealthServer } from './server/createHealthServer.js';
|
|
43
46
|
export { TaskService } from './server/tasks/TaskService.js';
|
|
44
47
|
export { AgreementService } from './server/agreements/AgreementService.js';
|
|
@@ -61,9 +64,7 @@ export { classifyIncomingEvent } from './context/classifyEnvelope.js';
|
|
|
61
64
|
export { buildContextUpdates, CONTEXT_RESET } from './context/applyEffects.js';
|
|
62
65
|
export { getBatchEvents, isTaskResultRelevantToAgent } from './context/batch.js';
|
|
63
66
|
export { classifyWorkflowEvent, findTaskResult, unwrapBatchEvent, findIncomingTaskResult } from './context/routingLabels.js';
|
|
64
|
-
export {
|
|
65
|
-
export { runInboxCatchUp } from './server/InboxCatchUp.js';
|
|
66
|
-
export type { InboxCatchUpDeps, InboxCatchUpResult } from './server/InboxCatchUp.js';
|
|
67
|
+
export { MessagesClient, ArtifactsClient, AgentSearchClient, getBackendUrl, getWebSocketUrl, } from '@ziggs-ai/api-client';
|
|
67
68
|
export { SeenMessages } from './server/SeenMessages.js';
|
|
68
69
|
export { runtimeLog, resetRuntimeLogLevelCache } from './shared/runtimeLog.js';
|
|
69
70
|
export type { RuntimeLogLevel } from './shared/runtimeLog.js';
|
package/dist/index.js
CHANGED
|
@@ -28,11 +28,14 @@ export { KICKOFF_STATE, injectKickoffDefault, buildKickoffState } from './runtim
|
|
|
28
28
|
export { validateWorkflow, WorkflowValidationError } from './runtime/validateWorkflow.js';
|
|
29
29
|
export { deriveRespondsToProposals, statesRespondToProposals, } from './runtime/deriveRespondsToProposals.js';
|
|
30
30
|
export { outcomeFromEvent, outcomeFromActionResult, applyOutcomeToCtx } from './runtime/buildOutcome.js';
|
|
31
|
+
export { createWakeWriteKeys } from './runtime/wakeWriteKeys.js';
|
|
31
32
|
export { default } from './AgentHost.js';
|
|
32
33
|
// ── Supporting modules ──────────────────────────────────────────────────────
|
|
33
34
|
export { validateContext, ContextWarning } from './cognition/validateContext.js';
|
|
34
35
|
export { runLauncher } from './server/runLauncher.js';
|
|
35
36
|
export { ConnectionPool } from './server/ConnectionPool.js';
|
|
37
|
+
export { createStaleBacklogGate, resolveReplayPolicy, newestActionableAt, parseReplayMaxAge, } from './server/staleBacklogGate.js';
|
|
38
|
+
export { decodeOperatorKeyClaims, boundKeyFleetWarning } from './shared/operatorKeyClaims.js';
|
|
36
39
|
export { createHealthServer } from './server/createHealthServer.js';
|
|
37
40
|
export { TaskService } from './server/tasks/TaskService.js';
|
|
38
41
|
export { AgreementService } from './server/agreements/AgreementService.js';
|
|
@@ -66,7 +69,6 @@ export { classifyIncomingEvent } from './context/classifyEnvelope.js';
|
|
|
66
69
|
export { buildContextUpdates, CONTEXT_RESET } from './context/applyEffects.js';
|
|
67
70
|
export { getBatchEvents, isTaskResultRelevantToAgent } from './context/batch.js';
|
|
68
71
|
export { classifyWorkflowEvent, findTaskResult, unwrapBatchEvent, findIncomingTaskResult } from './context/routingLabels.js';
|
|
69
|
-
export {
|
|
70
|
-
export { runInboxCatchUp } from './server/InboxCatchUp.js';
|
|
72
|
+
export { MessagesClient, ArtifactsClient, AgentSearchClient, getBackendUrl, getWebSocketUrl, } from '@ziggs-ai/api-client';
|
|
71
73
|
export { SeenMessages } from './server/SeenMessages.js';
|
|
72
74
|
export { runtimeLog, resetRuntimeLogLevelCache } from './shared/runtimeLog.js';
|
|
@@ -4,6 +4,12 @@ export interface NormalizedEvent {
|
|
|
4
4
|
senderId?: string;
|
|
5
5
|
senderType?: string;
|
|
6
6
|
receiverId?: string;
|
|
7
|
+
/**
|
|
8
|
+
* Backend per-delivery id of the chat row this event came from. The one
|
|
9
|
+
* field of the event that is identical on live push and on catch-up
|
|
10
|
+
* redelivery, so it anchors the wake's idempotency keys (wakeWriteKeys).
|
|
11
|
+
*/
|
|
12
|
+
messageId?: string;
|
|
7
13
|
timestamp: number;
|
|
8
14
|
type: 'task_result' | 'message' | 'agreement_lifecycle';
|
|
9
15
|
operation?: string;
|
|
@@ -11,8 +17,6 @@ export interface NormalizedEvent {
|
|
|
11
17
|
result?: unknown;
|
|
12
18
|
text?: string;
|
|
13
19
|
};
|
|
14
|
-
shouldProcess: boolean;
|
|
15
|
-
reason: string | null;
|
|
16
20
|
}
|
|
17
21
|
interface WireParties {
|
|
18
22
|
creator?: string;
|
|
@@ -36,11 +40,10 @@ interface WireTask {
|
|
|
36
40
|
}
|
|
37
41
|
interface WireMetadata {
|
|
38
42
|
chatId?: string;
|
|
39
|
-
|
|
43
|
+
messageId?: string;
|
|
40
44
|
entryType?: string;
|
|
41
45
|
content_type?: string;
|
|
42
46
|
contentType?: string;
|
|
43
|
-
receiverId?: string;
|
|
44
47
|
operation?: string;
|
|
45
48
|
agreementId?: string;
|
|
46
49
|
sender?: {
|
|
@@ -50,10 +53,13 @@ interface WireMetadata {
|
|
|
50
53
|
receiver?: {
|
|
51
54
|
id?: string;
|
|
52
55
|
};
|
|
53
|
-
to?: {
|
|
54
|
-
id?: string;
|
|
55
|
-
};
|
|
56
56
|
task?: WireTask;
|
|
57
|
+
service?: {
|
|
58
|
+
task?: WireTask;
|
|
59
|
+
operation?: string;
|
|
60
|
+
agreement?: unknown;
|
|
61
|
+
agreementId?: string;
|
|
62
|
+
};
|
|
57
63
|
[key: string]: unknown;
|
|
58
64
|
}
|
|
59
65
|
interface NormalizeArgs {
|
|
@@ -61,5 +67,14 @@ interface NormalizeArgs {
|
|
|
61
67
|
metadata?: WireMetadata;
|
|
62
68
|
ownAgentId?: string | null;
|
|
63
69
|
}
|
|
64
|
-
|
|
70
|
+
/**
|
|
71
|
+
* Classify + shape a wire delivery into a lane-routable event. Targeting is
|
|
72
|
+
* backend policy now (delivery law, phase 4): the inbox loop only ever delivers
|
|
73
|
+
* what is fenced to this agent, so there is no client-side "is this for me"
|
|
74
|
+
* relevance gate — this function decides SHAPE (message vs task_result vs
|
|
75
|
+
* agreement_lifecycle) and the fields the FSM / SessionResolver / wakeWriteKeys
|
|
76
|
+
* depend on, nothing more. `ownAgentId` is accepted for call-site compatibility
|
|
77
|
+
* but no longer gates anything.
|
|
78
|
+
*/
|
|
79
|
+
export declare function normalizeIncomingEvent({ text, metadata }: NormalizeArgs): NormalizedEvent;
|
|
65
80
|
export {};
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { OPEN_AGREEMENT_TARGET } from '@ziggs-ai/api-client';
|
|
2
1
|
/** Backend wire operations that map to FSM `proposal-resolved`. */
|
|
3
2
|
const PROPOSAL_RESOLVED_OPERATIONS = new Set([
|
|
4
3
|
'proposal_approved',
|
|
@@ -6,10 +5,10 @@ const PROPOSAL_RESOLVED_OPERATIONS = new Set([
|
|
|
6
5
|
'proposal_approved_execute',
|
|
7
6
|
]);
|
|
8
7
|
function normalizeChatId(metadata) {
|
|
9
|
-
return metadata.chatId ||
|
|
8
|
+
return metadata.chatId || null;
|
|
10
9
|
}
|
|
11
10
|
function normalizeReceiverId(metadata, taskData) {
|
|
12
|
-
return metadata.receiver?.id ||
|
|
11
|
+
return metadata.receiver?.id || taskData?.executorId || taskData?.proposedTo || null;
|
|
13
12
|
}
|
|
14
13
|
function isTaskRelated(metadata, taskData) {
|
|
15
14
|
if (!taskData?.taskId)
|
|
@@ -20,20 +19,26 @@ function isTaskRelated(metadata, taskData) {
|
|
|
20
19
|
|| contentType === 'task' || contentType === 'task_update'
|
|
21
20
|
|| Boolean(taskData.state || taskData.proposal || taskData.parentTaskId || taskData.executorId || taskData.agentId || taskData.payerId || taskData.createdBy));
|
|
22
21
|
}
|
|
23
|
-
function isRelevantForAgent(ids, ownAgentId) {
|
|
24
|
-
if (!ownAgentId)
|
|
25
|
-
return true;
|
|
26
|
-
return ids.includes(ownAgentId) || ids.includes(OPEN_AGREEMENT_TARGET);
|
|
27
|
-
}
|
|
28
22
|
function isAgreementLifecycleNotification(metadata) {
|
|
29
23
|
const entryType = metadata.entryType || 'message';
|
|
30
|
-
const operation = metadata.operation;
|
|
24
|
+
const operation = metadata.service?.operation ?? metadata.operation;
|
|
31
25
|
return (entryType === 'notification' &&
|
|
32
26
|
typeof operation === 'string' &&
|
|
33
27
|
PROPOSAL_RESOLVED_OPERATIONS.has(operation));
|
|
34
28
|
}
|
|
35
|
-
|
|
36
|
-
|
|
29
|
+
/**
|
|
30
|
+
* Classify + shape a wire delivery into a lane-routable event. Targeting is
|
|
31
|
+
* backend policy now (delivery law, phase 4): the inbox loop only ever delivers
|
|
32
|
+
* what is fenced to this agent, so there is no client-side "is this for me"
|
|
33
|
+
* relevance gate — this function decides SHAPE (message vs task_result vs
|
|
34
|
+
* agreement_lifecycle) and the fields the FSM / SessionResolver / wakeWriteKeys
|
|
35
|
+
* depend on, nothing more. `ownAgentId` is accepted for call-site compatibility
|
|
36
|
+
* but no longer gates anything.
|
|
37
|
+
*/
|
|
38
|
+
export function normalizeIncomingEvent({ text, metadata = {} }) {
|
|
39
|
+
// ZIG-936: classifier data lives under `service` on the inbox-pull wire;
|
|
40
|
+
// fall back to top-level for hand-built (direct-injection) metadata.
|
|
41
|
+
const taskData = (metadata.service?.task ?? metadata.task) || null;
|
|
37
42
|
const chatId = normalizeChatId(metadata);
|
|
38
43
|
const receiverId = normalizeReceiverId(metadata, taskData);
|
|
39
44
|
const senderType = metadata.sender?.type ? String(metadata.sender.type).toUpperCase() : undefined;
|
|
@@ -41,48 +46,32 @@ export function normalizeIncomingEvent({ text, metadata = {}, ownAgentId = null
|
|
|
41
46
|
senderId: metadata.sender?.id,
|
|
42
47
|
senderType,
|
|
43
48
|
receiverId: receiverId || undefined,
|
|
49
|
+
messageId: typeof metadata.messageId === 'string' && metadata.messageId ? metadata.messageId : undefined,
|
|
44
50
|
timestamp: Date.now(),
|
|
45
51
|
};
|
|
46
52
|
if (isAgreementLifecycleNotification(metadata)) {
|
|
47
|
-
const
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
receiverId === OPEN_AGREEMENT_TARGET;
|
|
51
|
-
const agreementId = typeof metadata.agreementId === 'string' ? metadata.agreementId : '';
|
|
53
|
+
const operation = metadata.service?.operation ?? metadata.operation;
|
|
54
|
+
const rawAgreementId = metadata.service?.agreementId ?? metadata.agreementId;
|
|
55
|
+
const agreementId = typeof rawAgreementId === 'string' ? rawAgreementId : '';
|
|
52
56
|
return {
|
|
53
57
|
chatId,
|
|
54
58
|
event: {
|
|
55
59
|
...base,
|
|
56
60
|
type: 'agreement_lifecycle',
|
|
57
|
-
operation
|
|
61
|
+
operation,
|
|
58
62
|
agreementId,
|
|
59
63
|
text,
|
|
60
64
|
},
|
|
61
|
-
shouldProcess: relevant,
|
|
62
|
-
reason: relevant ? null : 'message_not_targeted_to_agent',
|
|
63
65
|
};
|
|
64
66
|
}
|
|
65
67
|
if (isTaskRelated(metadata, taskData)) {
|
|
66
|
-
const receiverFromMeta = metadata.receiver?.id || metadata.receiverId || null;
|
|
67
|
-
const parties = taskData.agreement?.parties ?? {};
|
|
68
|
-
const candidateIds = [
|
|
69
|
-
parties.creator, parties.provider, parties.payer, parties.proposedTo,
|
|
70
|
-
taskData.agentId, taskData.executorId, taskData.payerId, taskData.proposedTo, taskData.createdBy,
|
|
71
|
-
receiverFromMeta,
|
|
72
|
-
].filter(Boolean);
|
|
73
|
-
const relevant = isRelevantForAgent(candidateIds, ownAgentId);
|
|
74
68
|
return {
|
|
75
69
|
chatId,
|
|
76
70
|
event: { ...base, type: 'task_result', result: taskData, text },
|
|
77
|
-
shouldProcess: relevant,
|
|
78
|
-
reason: relevant ? null : 'task_not_targeted_to_agent',
|
|
79
71
|
};
|
|
80
72
|
}
|
|
81
|
-
const relevant = !ownAgentId || !receiverId || receiverId === ownAgentId || receiverId === OPEN_AGREEMENT_TARGET;
|
|
82
73
|
return {
|
|
83
74
|
chatId,
|
|
84
75
|
event: { ...base, type: 'message', text },
|
|
85
|
-
shouldProcess: relevant,
|
|
86
|
-
reason: relevant ? null : 'message_not_targeted_to_agent',
|
|
87
76
|
};
|
|
88
77
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { Ctx, Outcome, Workflow, Identity, EffectHandler } from '../types.js';
|
|
1
|
+
import type { Ctx, Outcome, Workflow, Identity, EffectHandler, WakeWriteKeys } 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';
|
|
@@ -59,6 +59,8 @@ interface MachineOptions {
|
|
|
59
59
|
transitionTap?: (event: TransitionEvent) => void;
|
|
60
60
|
/** Runaway-loop backstop: max thinking turns without human input before parking. Default 100. */
|
|
61
61
|
maxTurnsWithoutUserInput?: number;
|
|
62
|
+
/** This wake's replay-stable write keys — one stream shared by every turn of the tick. */
|
|
63
|
+
wakeKeys?: WakeWriteKeys;
|
|
62
64
|
}
|
|
63
65
|
interface IncomingFrame {
|
|
64
66
|
event?: RawEvent;
|
|
@@ -84,6 +86,7 @@ export declare class AgentMachine {
|
|
|
84
86
|
toolManager: ToolManager;
|
|
85
87
|
promptBuilder: PromptBuilder;
|
|
86
88
|
memory?: MemoryStore;
|
|
89
|
+
wakeKeys?: WakeWriteKeys;
|
|
87
90
|
state: string;
|
|
88
91
|
status: 'active' | 'stopped';
|
|
89
92
|
ctx: Ctx;
|
|
@@ -111,7 +114,7 @@ export declare class AgentMachine {
|
|
|
111
114
|
output: number;
|
|
112
115
|
};
|
|
113
116
|
get stateTimeoutUnhandled(): boolean;
|
|
114
|
-
constructor({ workflow, executionCore, eff, toolManager, promptBuilder, memory, input, transitionTap, maxTurnsWithoutUserInput }: MachineOptions);
|
|
117
|
+
constructor({ workflow, executionCore, eff, toolManager, promptBuilder, memory, input, transitionTap, maxTurnsWithoutUserInput, wakeKeys }: MachineOptions);
|
|
115
118
|
getSnapshot(): MachineSnapshot;
|
|
116
119
|
/** Parked = currently in a parked state and not executing a turn. */
|
|
117
120
|
get isParked(): boolean;
|
|
@@ -30,6 +30,7 @@ export class AgentMachine {
|
|
|
30
30
|
toolManager;
|
|
31
31
|
promptBuilder;
|
|
32
32
|
memory;
|
|
33
|
+
wakeKeys;
|
|
33
34
|
state;
|
|
34
35
|
status = 'active';
|
|
35
36
|
ctx;
|
|
@@ -49,13 +50,14 @@ export class AgentMachine {
|
|
|
49
50
|
get llmCalls() { return this._llmCalls; }
|
|
50
51
|
get tokens() { return this._tokens; }
|
|
51
52
|
get stateTimeoutUnhandled() { return this._stateTimeoutUnhandled; }
|
|
52
|
-
constructor({ workflow, executionCore, eff, toolManager, promptBuilder, memory, input, transitionTap, maxTurnsWithoutUserInput }) {
|
|
53
|
+
constructor({ workflow, executionCore, eff, toolManager, promptBuilder, memory, input, transitionTap, maxTurnsWithoutUserInput, wakeKeys }) {
|
|
53
54
|
this.workflow = workflow;
|
|
54
55
|
this.executionCore = executionCore;
|
|
55
56
|
this.eff = eff;
|
|
56
57
|
this.toolManager = toolManager;
|
|
57
58
|
this.promptBuilder = promptBuilder;
|
|
58
59
|
this.memory = memory;
|
|
60
|
+
this.wakeKeys = wakeKeys;
|
|
59
61
|
this._transitionTap = transitionTap;
|
|
60
62
|
this._maxTurnsWithoutUserInput = maxTurnsWithoutUserInput ?? DEFAULT_MAX_TURNS_WITHOUT_USER_INPUT;
|
|
61
63
|
this.state = input.state || workflow.initial;
|
|
@@ -200,6 +202,7 @@ export class AgentMachine {
|
|
|
200
202
|
eff: this.eff,
|
|
201
203
|
memory: this.memory,
|
|
202
204
|
incomingEvent: this._lastIncomingEvent,
|
|
205
|
+
wakeKeys: this.wakeKeys,
|
|
203
206
|
});
|
|
204
207
|
this.ctx.toolResults = toolResults;
|
|
205
208
|
this._llmCalls += llmCalls;
|
|
@@ -293,6 +296,7 @@ export class AgentMachine {
|
|
|
293
296
|
promptBuilder: this.promptBuilder,
|
|
294
297
|
memory: this.memory,
|
|
295
298
|
definition: this.workflow,
|
|
299
|
+
wakeKeys: this.wakeKeys,
|
|
296
300
|
});
|
|
297
301
|
this.ctx.toolResults = toolResults;
|
|
298
302
|
this._llmCalls += llmCalls;
|
|
@@ -352,7 +356,9 @@ export class AgentMachine {
|
|
|
352
356
|
if (!userId)
|
|
353
357
|
return;
|
|
354
358
|
try {
|
|
355
|
-
|
|
359
|
+
// Wake-derived messageId: a crash-replay that errors the same way parks
|
|
360
|
+
// again and re-sends this apology — same key, backend drops the dupe.
|
|
361
|
+
await this.eff({ kind: 'send-message', sessionId: this.ctx.identity.sessionId, receiverId: userId, text, messageId: this.wakeKeys?.messageKey() });
|
|
356
362
|
}
|
|
357
363
|
catch { /* best-effort */ }
|
|
358
364
|
}
|
|
@@ -421,8 +421,8 @@ ${this._renderDecisionSchemaFromActions(actions, senderMeta, ctxObj, omitMessage
|
|
|
421
421
|
for (let i = history.length - 1; i >= 0; i--) {
|
|
422
422
|
const row = history[i];
|
|
423
423
|
const sender = row.sender;
|
|
424
|
-
const id = String(sender?.id ||
|
|
425
|
-
const type = String(sender?.type ||
|
|
424
|
+
const id = String(sender?.id || '');
|
|
425
|
+
const type = String(sender?.type || '').toUpperCase();
|
|
426
426
|
if (id && id !== myId && !isNonRoutableSenderType(type)) {
|
|
427
427
|
defaultReplyReceiverId = id;
|
|
428
428
|
break;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { Ctx, EffectHandler, MechanicalState, Outcome, ToolResultEntry } from '../types.js';
|
|
1
|
+
import type { Ctx, EffectHandler, MechanicalState, Outcome, ToolResultEntry, WakeWriteKeys } 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 {
|
|
@@ -22,4 +22,6 @@ export declare function runMechanical(input: {
|
|
|
22
22
|
eff: EffectHandler;
|
|
23
23
|
memory?: MemoryStore;
|
|
24
24
|
incomingEvent: RawEvent | null;
|
|
25
|
+
/** This wake's replay-stable write keys (shared across every turn of the tick — see AgentMachine). */
|
|
26
|
+
wakeKeys?: WakeWriteKeys;
|
|
25
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 } = input;
|
|
8
|
+
const { stateId, state, ctx, eff, memory, incomingEvent, wakeKeys } = 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, wakeKeys });
|
|
14
14
|
}
|
|
15
15
|
return eff(effect);
|
|
16
16
|
};
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { Action, Ctx, Outcome, ToolResultEntry, EffectHandler } from '../types.js';
|
|
1
|
+
import type { Action, Ctx, Outcome, ToolResultEntry, EffectHandler, WakeWriteKeys } 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';
|
|
@@ -32,6 +32,8 @@ interface RunTurnInput {
|
|
|
32
32
|
* to disable it without a code change.
|
|
33
33
|
*/
|
|
34
34
|
streamRespondActions?: boolean;
|
|
35
|
+
/** This wake's replay-stable write keys (shared across every turn of the tick — see AgentMachine). */
|
|
36
|
+
wakeKeys?: WakeWriteKeys;
|
|
35
37
|
}
|
|
36
38
|
interface RunTurnResult {
|
|
37
39
|
outcome: Outcome;
|
package/dist/runtime/runTurn.js
CHANGED
|
@@ -45,17 +45,28 @@ 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 } = input;
|
|
48
|
+
const { stateId, statePrompt, actions, incomingEvent, ctx, eff, toolManager, promptBuilder, memory, definition, wakeKeys } = 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';
|
|
52
52
|
const sessionId = ctx.identity.sessionId;
|
|
53
53
|
const runId = crypto.randomUUID();
|
|
54
|
-
// Wrap eff to stamp runId + stateId onto llm-call and tool-call effects
|
|
54
|
+
// Wrap eff to stamp runId + stateId onto llm-call and tool-call effects,
|
|
55
|
+
// and the wake's replay-stable write keys onto tool-call effects.
|
|
56
|
+
// Send-message effects without an explicit messageId get a wake-derived one
|
|
57
|
+
// here — a crash-replay of the wake re-derives the same id and the backend's
|
|
58
|
+
// (chatId, messageId) dedup drops the duplicate (ZIG-718). Undefined on
|
|
59
|
+
// keyless wakes, where the transport generates a fresh id as before.
|
|
55
60
|
const effWithTrace = (effect) => {
|
|
56
|
-
if (effect.kind === 'llm-call'
|
|
61
|
+
if (effect.kind === 'llm-call') {
|
|
57
62
|
return eff({ ...effect, runId, stateId });
|
|
58
63
|
}
|
|
64
|
+
if (effect.kind === 'tool-call') {
|
|
65
|
+
return eff({ ...effect, runId, stateId, wakeKeys });
|
|
66
|
+
}
|
|
67
|
+
if (effect.kind === 'send-message' && !effect.messageId) {
|
|
68
|
+
return eff({ ...effect, messageId: wakeKeys?.messageKey() });
|
|
69
|
+
}
|
|
59
70
|
return eff(effect);
|
|
60
71
|
};
|
|
61
72
|
const emitThought = async (text) => {
|
|
@@ -65,7 +76,7 @@ export async function runTurn(input) {
|
|
|
65
76
|
if (s.length === 0)
|
|
66
77
|
return;
|
|
67
78
|
try {
|
|
68
|
-
await eff({ kind: 'record-event', sessionId, entry: { kind: 'thought', text: s } });
|
|
79
|
+
await eff({ kind: 'record-event', sessionId, entry: { kind: 'thought', text: s }, idempotencyKey: wakeKeys?.artifactKey() });
|
|
69
80
|
}
|
|
70
81
|
catch (err) {
|
|
71
82
|
runtimeLog.warn('runTurn', `record-event:thought failed: ${err.message}`);
|
|
@@ -295,7 +306,10 @@ export async function runTurn(input) {
|
|
|
295
306
|
const inboundFromAgent = String(incomingEvent?.senderType || '').toUpperCase() === 'AGENT';
|
|
296
307
|
runtimeLog.info('runTurn', `streaming check: enabled=${streamRespondActions} receiverId=${decision.receiverId ?? 'none'} tool=${actionDef?.tool ?? 'none'} action=${chosenAction} inboundFromAgent=${inboundFromAgent}`);
|
|
297
308
|
if (streamRespondActions && !inboundFromAgent && decision.receiverId && !actionDef?.tool && chosenAction !== 'wait') {
|
|
298
|
-
|
|
309
|
+
// Wake-derived when the wake has a durable identity: a crash-replay
|
|
310
|
+
// streams and persists under the SAME id, so the backend's
|
|
311
|
+
// (chatId, messageId) dedup drops the duplicate reply (ZIG-718).
|
|
312
|
+
const streamMessageId = wakeKeys?.messageKey() ?? crypto.randomUUID();
|
|
299
313
|
runtimeLog.info('runTurn', `stream-text start action=${chosenAction} receiverId=${decision.receiverId} msgId=${streamMessageId}`);
|
|
300
314
|
const streamMessages = [
|
|
301
315
|
...messages,
|
|
@@ -362,7 +376,9 @@ export async function runTurn(input) {
|
|
|
362
376
|
}
|
|
363
377
|
let result;
|
|
364
378
|
try {
|
|
365
|
-
|
|
379
|
+
// effWithTrace, not eff: JSON-path tool calls must carry runId/stateId
|
|
380
|
+
// and the wake's write keys exactly like tool-loop calls do.
|
|
381
|
+
result = await executeDecision(decision, actionDef, effWithTrace, sessionId, memory, null, addToolUsage);
|
|
366
382
|
}
|
|
367
383
|
catch (err) {
|
|
368
384
|
// Only outbound sends can throw out of executeDecision — executeTool
|
|
@@ -409,8 +425,10 @@ export async function runTurn(input) {
|
|
|
409
425
|
}
|
|
410
426
|
}
|
|
411
427
|
if (!turnError) {
|
|
428
|
+
// effWithTrace, not eff: the echoed summary is a user-visible send and
|
|
429
|
+
// must carry a wake-derived messageId like every other send of the turn.
|
|
412
430
|
await echoOptInToolSummaryIfNeeded({
|
|
413
|
-
allEmittedEvents, eff, sessionId,
|
|
431
|
+
allEmittedEvents, eff: effWithTrace, sessionId,
|
|
414
432
|
context: ctxObj, toolManager, incomingEvent,
|
|
415
433
|
});
|
|
416
434
|
}
|
|
@@ -791,8 +809,8 @@ function pickPrimaryUserIdFromContext(context) {
|
|
|
791
809
|
for (let i = hist.length - 1; i >= 0; i--) {
|
|
792
810
|
const row = hist[i];
|
|
793
811
|
const sender = row?.sender;
|
|
794
|
-
const type = String(sender?.type ||
|
|
795
|
-
const id =
|
|
812
|
+
const type = String(sender?.type || '').toUpperCase();
|
|
813
|
+
const id = sender?.id;
|
|
796
814
|
if (type === 'USER' && id)
|
|
797
815
|
return id;
|
|
798
816
|
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { WakeWriteKeys } from '../types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Build the per-wake key derivation. Call once per tick with the lane and the
|
|
4
|
+
* raw triggering event, then thread the instance to every write of the wake.
|
|
5
|
+
*
|
|
6
|
+
* Key shapes (all well under the backend's 128-char cap):
|
|
7
|
+
* - artifacts: `wk_<wake-hash>_a<ordinal>` — ordinal increments per
|
|
8
|
+
* artifact write, so a wake writing several breadcrumbs keys each one.
|
|
9
|
+
* - task transitions: `wk_<wake-hash>_t_<hash(taskId, state)>` — derived from
|
|
10
|
+
* the transition itself, so it dedups regardless of the order in which the
|
|
11
|
+
* replayed wake happens to re-issue its tool calls.
|
|
12
|
+
* - chat messages: `sdk_idem_wk_<wake-hash>_m<ordinal>` — used directly as
|
|
13
|
+
* the messageId (the backend dedups on (chatId, messageId), ZIG-718), drawn
|
|
14
|
+
* from the same ordinal stream as artifacts so one counter keys the wake's
|
|
15
|
+
* whole write sequence. The prefix matches the tool-layer convention for
|
|
16
|
+
* deterministic ids (`sdk_idem_<llm-key>`).
|
|
17
|
+
* - task spawns: `wk_<wake-hash>_s_<hash(agreement, description, …)>` —
|
|
18
|
+
* derived from what the spawn writes, so it dedups regardless of re-issue
|
|
19
|
+
* order while still distinguishing multiple spawns in one wake.
|
|
20
|
+
* - agreement create: `wk_<wake-hash>_ap_<hash(…)>` (propose) and
|
|
21
|
+
* `wk_<wake-hash>_asc_<hash(…)>` (subcontract) — same content-derived scheme
|
|
22
|
+
* as task spawns; the backend dedups on (creator, key) so a replayed
|
|
23
|
+
* propose/subcontract resolves to the original agreement.
|
|
24
|
+
*/
|
|
25
|
+
export declare function createWakeWriteKeys(laneKey: string, event: unknown): WakeWriteKeys;
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { getBatchEvents } from '../context/batch.js';
|
|
3
|
+
/**
|
|
4
|
+
* Deterministic idempotency keys for hosted-agent writes (delivery law, item 2).
|
|
5
|
+
*
|
|
6
|
+
* A crash between acting and acking redelivers the SAME wake event; the agent
|
|
7
|
+
* re-thinks and re-writes. The backend drops duplicate artifact writes and
|
|
8
|
+
* no-ops duplicate task transitions — but only when the replayed wake derives
|
|
9
|
+
* the SAME key. So keys are a pure function of durable wake inputs (laneKey +
|
|
10
|
+
* the triggering event's delivery identity + a per-wake write ordinal). They
|
|
11
|
+
* are never LLM-supplied and never random: asking the model to invent a key
|
|
12
|
+
* makes replay-stability a matter of sampling temperature.
|
|
13
|
+
*
|
|
14
|
+
* One instance is created per wake (tick) and threaded to every write the wake
|
|
15
|
+
* performs — a second instance created for the same (laneKey, event), as a
|
|
16
|
+
* crash-replay does, reproduces the same key sequence.
|
|
17
|
+
*/
|
|
18
|
+
/**
|
|
19
|
+
* Durable identity of the event that triggered this wake, or null when it has
|
|
20
|
+
* none. Null means the wake is not a redelivery candidate (timer and other
|
|
21
|
+
* host-synthesized frames fire fresh each time), so its writes carry no key —
|
|
22
|
+
* a derived-anyway key could only collide across genuinely distinct wakes and
|
|
23
|
+
* silently swallow their writes, which is worse than the duplicate it prevents.
|
|
24
|
+
*/
|
|
25
|
+
function eventIdentity(event) {
|
|
26
|
+
if (!event || typeof event !== 'object')
|
|
27
|
+
return null;
|
|
28
|
+
const e = event;
|
|
29
|
+
if (e.type === 'batch') {
|
|
30
|
+
// EventQueue coalesces queued events into one frame. Identify the batch by
|
|
31
|
+
// its identifiable members (sorted — arrival order is not replay-stable).
|
|
32
|
+
// Members without identity are ignored rather than nulling the whole batch:
|
|
33
|
+
// an identified member can only recur via redelivery, which is exactly when
|
|
34
|
+
// matching keys are wanted.
|
|
35
|
+
const parts = getBatchEvents(event)
|
|
36
|
+
.map(eventIdentity)
|
|
37
|
+
.filter((p) => p !== null)
|
|
38
|
+
.sort();
|
|
39
|
+
return parts.length > 0 ? parts.join('|') : null;
|
|
40
|
+
}
|
|
41
|
+
// Per-delivery id stamped by the backend on chat rows — present on live
|
|
42
|
+
// pushes and on catch-up redelivery alike, and the most precise identity.
|
|
43
|
+
if (typeof e.messageId === 'string' && e.messageId)
|
|
44
|
+
return `msg:${e.messageId}`;
|
|
45
|
+
if (e.type === 'task_result') {
|
|
46
|
+
// Host-synthesized task frames (bootstrap of already-assigned tasks) carry
|
|
47
|
+
// no messageId; the task snapshot itself is the identity. Re-bootstrapping
|
|
48
|
+
// the same task in the same state IS re-processing, so the collision this
|
|
49
|
+
// produces is the dedup we want.
|
|
50
|
+
const result = (e.result ?? {});
|
|
51
|
+
const taskId = typeof result.taskId === 'string' ? result.taskId : null;
|
|
52
|
+
if (!taskId)
|
|
53
|
+
return null;
|
|
54
|
+
const state = typeof result.state === 'string' ? result.state : '';
|
|
55
|
+
const operation = typeof result._operation === 'string' ? result._operation : '';
|
|
56
|
+
return `task:${taskId}:${state}:${operation}`;
|
|
57
|
+
}
|
|
58
|
+
if (e.type === 'agreement_lifecycle') {
|
|
59
|
+
const agreementId = typeof e.agreementId === 'string' ? e.agreementId : null;
|
|
60
|
+
if (!agreementId)
|
|
61
|
+
return null;
|
|
62
|
+
return `agr:${agreementId}:${typeof e.operation === 'string' ? e.operation : ''}`;
|
|
63
|
+
}
|
|
64
|
+
// state-timeout, resource_changed, and anything else: no per-delivery id on
|
|
65
|
+
// the wire, and never redelivered by catch-up — keyless by design.
|
|
66
|
+
return null;
|
|
67
|
+
}
|
|
68
|
+
function sha(input) {
|
|
69
|
+
return createHash('sha256').update(input).digest('hex');
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Build the per-wake key derivation. Call once per tick with the lane and the
|
|
73
|
+
* raw triggering event, then thread the instance to every write of the wake.
|
|
74
|
+
*
|
|
75
|
+
* Key shapes (all well under the backend's 128-char cap):
|
|
76
|
+
* - artifacts: `wk_<wake-hash>_a<ordinal>` — ordinal increments per
|
|
77
|
+
* artifact write, so a wake writing several breadcrumbs keys each one.
|
|
78
|
+
* - task transitions: `wk_<wake-hash>_t_<hash(taskId, state)>` — derived from
|
|
79
|
+
* the transition itself, so it dedups regardless of the order in which the
|
|
80
|
+
* replayed wake happens to re-issue its tool calls.
|
|
81
|
+
* - chat messages: `sdk_idem_wk_<wake-hash>_m<ordinal>` — used directly as
|
|
82
|
+
* the messageId (the backend dedups on (chatId, messageId), ZIG-718), drawn
|
|
83
|
+
* from the same ordinal stream as artifacts so one counter keys the wake's
|
|
84
|
+
* whole write sequence. The prefix matches the tool-layer convention for
|
|
85
|
+
* deterministic ids (`sdk_idem_<llm-key>`).
|
|
86
|
+
* - task spawns: `wk_<wake-hash>_s_<hash(agreement, description, …)>` —
|
|
87
|
+
* derived from what the spawn writes, so it dedups regardless of re-issue
|
|
88
|
+
* order while still distinguishing multiple spawns in one wake.
|
|
89
|
+
* - agreement create: `wk_<wake-hash>_ap_<hash(…)>` (propose) and
|
|
90
|
+
* `wk_<wake-hash>_asc_<hash(…)>` (subcontract) — same content-derived scheme
|
|
91
|
+
* as task spawns; the backend dedups on (creator, key) so a replayed
|
|
92
|
+
* propose/subcontract resolves to the original agreement.
|
|
93
|
+
*/
|
|
94
|
+
export function createWakeWriteKeys(laneKey, event) {
|
|
95
|
+
const identity = eventIdentity(event);
|
|
96
|
+
const wakeHash = identity === null ? null : sha(`${laneKey}\n${identity}`).slice(0, 40);
|
|
97
|
+
let ordinal = 0;
|
|
98
|
+
return {
|
|
99
|
+
wakeKey: wakeHash === null ? null : `wk_${wakeHash}`,
|
|
100
|
+
artifactKey() {
|
|
101
|
+
if (wakeHash === null)
|
|
102
|
+
return undefined;
|
|
103
|
+
return `wk_${wakeHash}_a${ordinal++}`;
|
|
104
|
+
},
|
|
105
|
+
taskStateKey(taskId, state) {
|
|
106
|
+
if (wakeHash === null)
|
|
107
|
+
return undefined;
|
|
108
|
+
return `wk_${wakeHash}_t_${sha(`${taskId}\n${state}`).slice(0, 16)}`;
|
|
109
|
+
},
|
|
110
|
+
messageKey() {
|
|
111
|
+
if (wakeHash === null)
|
|
112
|
+
return undefined;
|
|
113
|
+
return `sdk_idem_wk_${wakeHash}_m${ordinal++}`;
|
|
114
|
+
},
|
|
115
|
+
taskSpawnKey(fields) {
|
|
116
|
+
if (wakeHash === null)
|
|
117
|
+
return undefined;
|
|
118
|
+
const spawnHash = sha(`${fields.agreementId}\n${fields.description}\n${fields.assigneeId ?? ''}\n${fields.parentTaskId ?? ''}`).slice(0, 16);
|
|
119
|
+
return `wk_${wakeHash}_s_${spawnHash}`;
|
|
120
|
+
},
|
|
121
|
+
agreementProposeKey(fields) {
|
|
122
|
+
if (wakeHash === null)
|
|
123
|
+
return undefined;
|
|
124
|
+
const proposeHash = sha(`${fields.proposedTo}\n${fields.providerId ?? ''}\n${fields.description}\n${fields.parentAgreementId ?? ''}`).slice(0, 16);
|
|
125
|
+
return `wk_${wakeHash}_ap_${proposeHash}`;
|
|
126
|
+
},
|
|
127
|
+
agreementSubcontractKey(fields) {
|
|
128
|
+
if (wakeHash === null)
|
|
129
|
+
return undefined;
|
|
130
|
+
const subHash = sha(`${fields.executorId}\n${fields.description}\n${fields.parentAgreementId}`).slice(0, 16);
|
|
131
|
+
return `wk_${wakeHash}_asc_${subHash}`;
|
|
132
|
+
},
|
|
133
|
+
};
|
|
134
|
+
}
|