@ziggs-ai/agent-sdk 0.1.12 → 0.2.1

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.
Files changed (218) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/README.md +41 -2
  3. package/dist/AgentHost.d.ts +201 -0
  4. package/dist/AgentHost.js +652 -0
  5. package/dist/adapters/AnthropicAdapter.d.ts +39 -0
  6. package/dist/adapters/AnthropicAdapter.js +200 -0
  7. package/dist/adapters/McpAdapter.d.ts +199 -0
  8. package/dist/adapters/McpAdapter.js +468 -0
  9. package/dist/adapters/OpenAIAdapter.d.ts +26 -0
  10. package/dist/adapters/OpenAIAdapter.js +158 -0
  11. package/dist/adapters/index.js +2 -0
  12. package/dist/agent/Agent.d.ts +78 -0
  13. package/dist/agent/Agent.js +58 -0
  14. package/dist/agent/ClaudeSdkAgent.d.ts +38 -0
  15. package/dist/agent/ClaudeSdkAgent.js +322 -0
  16. package/dist/agent/claudeCognition.d.ts +67 -0
  17. package/dist/agent/claudeCognition.js +103 -0
  18. package/dist/agent/claudeSdkProtocol.d.ts +11 -0
  19. package/dist/agent/claudeSdkTools.d.ts +19 -0
  20. package/dist/agent/claudeSdkTools.js +57 -0
  21. package/dist/agent/hostedAgentConnection.d.ts +15 -0
  22. package/dist/agent/hostedAgentConnection.js +29 -0
  23. package/dist/agent/toolManagerToSdkMcp.d.ts +12 -0
  24. package/dist/agent/toolManagerToSdkMcp.js +123 -0
  25. package/dist/brain/index.d.ts +50 -0
  26. package/dist/brain/index.js +45 -0
  27. package/dist/cognition/validateContext.d.ts +11 -0
  28. package/dist/cognition/validateContext.js +117 -0
  29. package/dist/context/applyEffects.d.ts +2 -0
  30. package/dist/context/applyEffects.js +89 -0
  31. package/dist/context/batch.d.ts +2 -0
  32. package/dist/context/batch.js +18 -0
  33. package/dist/context/classifyEnvelope.d.ts +1 -0
  34. package/dist/context/classifyEnvelope.js +58 -0
  35. package/dist/context/routingLabels.d.ts +4 -0
  36. package/dist/context/routingLabels.js +48 -0
  37. package/dist/defineAgent.d.ts +14 -0
  38. package/dist/defineAgent.js +68 -0
  39. package/dist/formatters/AgreementFormatter.d.ts +18 -0
  40. package/dist/formatters/AgreementFormatter.js +130 -0
  41. package/dist/formatters/HistoryFormatter.d.ts +26 -0
  42. package/dist/formatters/HistoryFormatter.js +173 -0
  43. package/dist/formatters/index.js +2 -0
  44. package/{src/index.ts → dist/index.d.ts} +7 -53
  45. package/dist/index.js +66 -0
  46. package/dist/ingress/normalizeIncoming.d.ts +65 -0
  47. package/dist/ingress/normalizeIncoming.js +88 -0
  48. package/dist/memory/MemoryStore.d.ts +55 -0
  49. package/dist/memory/MemoryStore.js +78 -0
  50. package/dist/pricing/fleetDefaults.d.ts +46 -0
  51. package/dist/pricing/fleetDefaults.js +168 -0
  52. package/dist/pricing/fleetEvalFree.d.ts +7 -0
  53. package/dist/pricing/fleetEvalFree.js +24 -0
  54. package/dist/pricing/fleetFreeTierA.gen.d.ts +6 -0
  55. package/{src/pricing/fleetFreeTierA.gen.ts → dist/pricing/fleetFreeTierA.gen.js} +5 -6
  56. package/dist/pricing/fleetTierByAgentId.gen.d.ts +7 -0
  57. package/dist/pricing/fleetTierByAgentId.gen.js +768 -0
  58. package/dist/runtime/AgentMachine.d.ts +143 -0
  59. package/dist/runtime/AgentMachine.js +400 -0
  60. package/dist/runtime/PromptBuilder.d.ts +96 -0
  61. package/dist/runtime/PromptBuilder.js +439 -0
  62. package/{src/runtime/bootstrapCtx.ts → dist/runtime/bootstrapCtx.d.ts} +1 -22
  63. package/dist/runtime/bootstrapCtx.js +37 -0
  64. package/dist/runtime/buildOutcome.d.ts +68 -0
  65. package/dist/runtime/buildOutcome.js +464 -0
  66. package/dist/runtime/defaults.d.ts +54 -0
  67. package/dist/runtime/defaults.js +47 -0
  68. package/dist/runtime/deriveRespondsToProposals.d.ts +9 -0
  69. package/dist/runtime/deriveRespondsToProposals.js +38 -0
  70. package/dist/runtime/kickoff.d.ts +15 -0
  71. package/dist/runtime/kickoff.js +112 -0
  72. package/dist/runtime/runMechanical.d.ts +25 -0
  73. package/dist/runtime/runMechanical.js +74 -0
  74. package/dist/runtime/runTurn.d.ts +63 -0
  75. package/dist/runtime/runTurn.js +800 -0
  76. package/dist/runtime/validateWorkflow.d.ts +17 -0
  77. package/dist/runtime/validateWorkflow.js +209 -0
  78. package/dist/server/ConnectionPool.d.ts +68 -0
  79. package/dist/server/ConnectionPool.js +166 -0
  80. package/dist/server/EventQueue.d.ts +33 -0
  81. package/dist/server/EventQueue.js +98 -0
  82. package/dist/server/InboxCatchUp.d.ts +24 -0
  83. package/dist/server/InboxCatchUp.js +214 -0
  84. package/dist/server/OutboxBuffer.d.ts +33 -0
  85. package/dist/server/OutboxBuffer.js +68 -0
  86. package/dist/server/SeenMessages.d.ts +13 -0
  87. package/dist/server/SeenMessages.js +31 -0
  88. package/dist/server/StateTimers.d.ts +34 -0
  89. package/dist/server/StateTimers.js +70 -0
  90. package/dist/server/ZiggsEffectHandler.d.ts +79 -0
  91. package/dist/server/ZiggsEffectHandler.js +319 -0
  92. package/dist/server/agreements/AgreementService.d.ts +50 -0
  93. package/dist/server/agreements/AgreementService.js +85 -0
  94. package/dist/server/createHealthServer.d.ts +7 -0
  95. package/dist/server/createHealthServer.js +75 -0
  96. package/dist/server/proactive/ProactiveTrigger.d.ts +33 -0
  97. package/dist/server/proactive/ProactiveTrigger.js +71 -0
  98. package/dist/server/runLauncher.d.ts +28 -0
  99. package/dist/server/runLauncher.js +93 -0
  100. package/dist/server/tasks/TaskService.d.ts +25 -0
  101. package/dist/server/tasks/TaskService.js +103 -0
  102. package/dist/server/tasks/index.js +1 -0
  103. package/dist/server/telemetryIngest.d.ts +24 -0
  104. package/dist/server/telemetryIngest.js +62 -0
  105. package/dist/server/tools/index.d.ts +24 -0
  106. package/{src/server/tools/index.ts → dist/server/tools/index.js} +4 -24
  107. package/dist/server/tools/tier1/protocolRunner.d.ts +39 -0
  108. package/dist/server/tools/tier1/protocolRunner.js +288 -0
  109. package/dist/server/tools/tier1/protocolTools.d.ts +12 -0
  110. package/dist/server/tools/tier1/protocolTools.js +37 -0
  111. package/dist/server/tools/tier2/connectionTools.d.ts +4 -0
  112. package/dist/server/tools/tier2/connectionTools.js +63 -0
  113. package/dist/server/tools/tier2/contextTools.d.ts +4 -0
  114. package/dist/server/tools/tier2/contextTools.js +46 -0
  115. package/dist/server/tools/tier2/discoveryTools.d.ts +4 -0
  116. package/dist/server/tools/tier2/discoveryTools.js +25 -0
  117. package/dist/server/tools/tier2/marketplaceTools.d.ts +3 -0
  118. package/dist/server/tools/tier2/marketplaceTools.js +22 -0
  119. package/dist/server/tools/tier2/paymentTools.d.ts +12 -0
  120. package/dist/server/tools/tier2/paymentTools.js +165 -0
  121. package/dist/server/ziggsconnect/ZiggsConnectClient.d.ts +35 -0
  122. package/dist/server/ziggsconnect/ZiggsConnectClient.js +95 -0
  123. package/dist/server/ziggscontext/ZiggsContextClient.d.ts +53 -0
  124. package/dist/server/ziggscontext/ZiggsContextClient.js +124 -0
  125. package/dist/server/ziggspay/ZiggsPayClient.d.ts +82 -0
  126. package/dist/server/ziggspay/ZiggsPayClient.js +203 -0
  127. package/dist/shared/ids.d.ts +1 -0
  128. package/dist/shared/ids.js +3 -0
  129. package/dist/shared/runtimeLog.d.ts +18 -0
  130. package/dist/shared/runtimeLog.js +68 -0
  131. package/dist/shared/types.d.ts +22 -0
  132. package/dist/shared/types.js +23 -0
  133. package/dist/tasks/protocolRegistry.d.ts +16 -0
  134. package/dist/tasks/protocolRegistry.js +20 -0
  135. package/dist/tools/ToolManager.d.ts +12 -0
  136. package/dist/tools/ToolManager.js +74 -0
  137. package/dist/tools/ToolProvider.d.ts +6 -0
  138. package/dist/tools/ToolProvider.js +21 -0
  139. package/dist/tools/defineTool.d.ts +27 -0
  140. package/dist/tools/defineTool.js +62 -0
  141. package/{src/tools/index.ts → dist/tools/index.d.ts} +0 -1
  142. package/dist/tools/index.js +6 -0
  143. package/dist/tools/recordReport.d.ts +41 -0
  144. package/dist/tools/recordReport.js +30 -0
  145. package/dist/types.d.ts +466 -0
  146. package/dist/types.js +7 -0
  147. package/dist/utils/jsonExtractor.d.ts +2 -0
  148. package/dist/utils/jsonExtractor.js +111 -0
  149. package/package.json +14 -6
  150. package/src/AgentHost.ts +0 -814
  151. package/src/adapters/AnthropicAdapter.ts +0 -238
  152. package/src/adapters/McpAdapter.ts +0 -678
  153. package/src/adapters/OpenAIAdapter.ts +0 -194
  154. package/src/agent/Agent.ts +0 -112
  155. package/src/agent/ClaudeSdkAgent.ts +0 -401
  156. package/src/agent/claudeCognition.ts +0 -139
  157. package/src/agent/claudeSdkTools.ts +0 -81
  158. package/src/agent/hostedAgentConnection.ts +0 -41
  159. package/src/agent/toolManagerToSdkMcp.ts +0 -136
  160. package/src/cognition/validateContext.ts +0 -95
  161. package/src/context/applyEffects.ts +0 -80
  162. package/src/context/batch.ts +0 -19
  163. package/src/context/classifyEnvelope.ts +0 -38
  164. package/src/context/routingLabels.ts +0 -46
  165. package/src/defineAgent.ts +0 -77
  166. package/src/formatters/AgreementFormatter.ts +0 -111
  167. package/src/formatters/HistoryFormatter.ts +0 -172
  168. package/src/ingress/normalizeIncoming.ts +0 -162
  169. package/src/memory/MemoryStore.ts +0 -104
  170. package/src/pricing/fleetDefaults.ts +0 -225
  171. package/src/pricing/fleetEvalFree.ts +0 -24
  172. package/src/pricing/fleetTierByAgentId.gen.ts +0 -775
  173. package/src/runtime/AgentMachine.ts +0 -484
  174. package/src/runtime/PromptBuilder.ts +0 -522
  175. package/src/runtime/buildOutcome.ts +0 -563
  176. package/src/runtime/defaults.ts +0 -86
  177. package/src/runtime/deriveRespondsToProposals.ts +0 -35
  178. package/src/runtime/kickoff.ts +0 -117
  179. package/src/runtime/runMechanical.ts +0 -106
  180. package/src/runtime/runTurn.ts +0 -870
  181. package/src/runtime/validateWorkflow.ts +0 -221
  182. package/src/server/ConnectionPool.ts +0 -206
  183. package/src/server/EventQueue.ts +0 -133
  184. package/src/server/InboxCatchUp.ts +0 -291
  185. package/src/server/OutboxBuffer.ts +0 -90
  186. package/src/server/SeenMessages.ts +0 -27
  187. package/src/server/StateTimers.ts +0 -85
  188. package/src/server/ZiggsEffectHandler.ts +0 -427
  189. package/src/server/agreements/AgreementService.ts +0 -118
  190. package/src/server/createHealthServer.ts +0 -85
  191. package/src/server/proactive/ProactiveTrigger.ts +0 -83
  192. package/src/server/runLauncher.ts +0 -146
  193. package/src/server/tasks/TaskService.ts +0 -111
  194. package/src/server/telemetryIngest.ts +0 -91
  195. package/src/server/tools/tier1/protocolRunner.ts +0 -353
  196. package/src/server/tools/tier1/protocolTools.ts +0 -114
  197. package/src/server/tools/tier2/connectionTools.ts +0 -75
  198. package/src/server/tools/tier2/contextTools.ts +0 -63
  199. package/src/server/tools/tier2/discoveryTools.ts +0 -34
  200. package/src/server/tools/tier2/marketplaceTools.ts +0 -27
  201. package/src/server/tools/tier2/paymentTools.ts +0 -170
  202. package/src/server/ziggsconnect/ZiggsConnectClient.ts +0 -126
  203. package/src/server/ziggscontext/ZiggsContextClient.ts +0 -129
  204. package/src/server/ziggspay/ZiggsPayClient.ts +0 -193
  205. package/src/shared/ids.ts +0 -3
  206. package/src/shared/runtimeLog.ts +0 -72
  207. package/src/shared/types.ts +0 -29
  208. package/src/tasks/protocolRegistry.ts +0 -25
  209. package/src/tools/ToolManager.ts +0 -95
  210. package/src/tools/ToolProvider.ts +0 -19
  211. package/src/tools/defineTool.ts +0 -90
  212. package/src/tools/recordReport.ts +0 -82
  213. package/src/types.ts +0 -483
  214. package/src/utils/jsonExtractor.ts +0 -100
  215. /package/{src/adapters/index.ts → dist/adapters/index.d.ts} +0 -0
  216. /package/{src/agent/claudeSdkProtocol.ts → dist/agent/claudeSdkProtocol.js} +0 -0
  217. /package/{src/formatters/index.ts → dist/formatters/index.d.ts} +0 -0
  218. /package/{src/server/tasks/index.ts → dist/server/tasks/index.d.ts} +0 -0
@@ -0,0 +1,78 @@
1
+ import type { Ctx, Identity, Workflow, EffectHandler } from '../types.js';
2
+ import { type TransitionEvent } from '../runtime/AgentMachine.js';
3
+ import { PromptBuilder } from '../runtime/PromptBuilder.js';
4
+ import type { MemoryStore } from '../memory/MemoryStore.js';
5
+ import type { ToolManager } from '../tools/ToolManager.js';
6
+ export interface AgentOptions {
7
+ workflow: Workflow;
8
+ toolManager: ToolManager;
9
+ promptBuilder?: PromptBuilder;
10
+ }
11
+ export interface TickInput {
12
+ /** Wire event (already normalized: a `message` or `task_result`-style envelope). */
13
+ event: unknown;
14
+ /** Server-provided per-session state. Omit on the first tick — a fresh ctx is constructed. */
15
+ ctx?: Ctx;
16
+ /** Server-provided workflow state name. Omit on the first tick — defaults to `workflow.initial`. */
17
+ state?: string;
18
+ /** Who am I and what session am I running in. The Agent carries this through Ctx. */
19
+ identity: Identity;
20
+ /** The single side channel for I/O — LLM, context, tools, send, record. */
21
+ eff: EffectHandler;
22
+ /**
23
+ * Agent-private long-term memory. Operator-provided (default
24
+ * `InMemoryStore` when omitted by `AgentHost`). Key shape is the agent's
25
+ * choice — `counterparty:<id>`, `agreement:<id>:notes`, etc.
26
+ */
27
+ memory?: MemoryStore;
28
+ /** Optional tap for FSM transition events (ZIG-442). */
29
+ transitionTap?: (event: TransitionEvent) => void;
30
+ }
31
+ export interface TickOutput {
32
+ /** Updated ctx — Server persists this against the session. */
33
+ ctx: Ctx;
34
+ /** Workflow state name after the tick — Server persists this against the session. */
35
+ state: string;
36
+ /** Whether the machine settled at a parked state. */
37
+ parked: boolean;
38
+ /** True when a state-timeout outcome matched no transition this tick — the host must NOT re-arm. */
39
+ stateTimeoutUnhandled: boolean;
40
+ /**
41
+ * Accumulated LLM call count and token usage for this tick.
42
+ * `cacheRead` / `cacheCreation` are populated by the Claude brain (its usage
43
+ * exposes cache traffic, which is the bulk of real cost — ZIG-759/813); the
44
+ * FSM path leaves them undefined. `total` includes cache tokens when present.
45
+ */
46
+ metrics: {
47
+ llmCalls: number;
48
+ tokens: {
49
+ total: number;
50
+ input: number;
51
+ output: number;
52
+ cacheRead?: number;
53
+ cacheCreation?: number;
54
+ };
55
+ };
56
+ }
57
+ /**
58
+ * Pure agent: workflow definition + cognition machinery. No I/O, no transport,
59
+ * no services. The Agent's only side channel is the `eff` handler it receives
60
+ * on each `tick`. Server-side state (sessions, lanes, queue, outbox) lives in
61
+ * `AgentHost` — not here.
62
+ *
63
+ * The contract:
64
+ *
65
+ * `tick({ event, ctx, state, identity, eff })` →
66
+ * `{ ctx, state, parked }`
67
+ *
68
+ * Reads top-to-bottom like a synchronous function. Every `await eff(...)`
69
+ * inside the cognition core is a yield to the Server, which performs the
70
+ * impure operation and resumes the Agent with the result.
71
+ */
72
+ export declare class Agent {
73
+ workflow: Workflow;
74
+ toolManager: ToolManager;
75
+ promptBuilder: PromptBuilder;
76
+ constructor({ workflow, toolManager, promptBuilder }: AgentOptions);
77
+ tick({ event, ctx, state, identity, eff, memory, transitionTap }: TickInput): Promise<TickOutput>;
78
+ }
@@ -0,0 +1,58 @@
1
+ import { AgentMachine } from '../runtime/AgentMachine.js';
2
+ import { runTurn } from '../runtime/runTurn.js';
3
+ import { PromptBuilder } from '../runtime/PromptBuilder.js';
4
+ /**
5
+ * Pure agent: workflow definition + cognition machinery. No I/O, no transport,
6
+ * no services. The Agent's only side channel is the `eff` handler it receives
7
+ * on each `tick`. Server-side state (sessions, lanes, queue, outbox) lives in
8
+ * `AgentHost` — not here.
9
+ *
10
+ * The contract:
11
+ *
12
+ * `tick({ event, ctx, state, identity, eff })` →
13
+ * `{ ctx, state, parked }`
14
+ *
15
+ * Reads top-to-bottom like a synchronous function. Every `await eff(...)`
16
+ * inside the cognition core is a yield to the Server, which performs the
17
+ * impure operation and resumes the Agent with the result.
18
+ */
19
+ export class Agent {
20
+ workflow;
21
+ toolManager;
22
+ promptBuilder;
23
+ constructor({ workflow, toolManager, promptBuilder }) {
24
+ if (!workflow?.states)
25
+ throw new Error('Agent: workflow with states is required');
26
+ if (!toolManager)
27
+ throw new Error('Agent: toolManager is required');
28
+ this.workflow = workflow;
29
+ this.toolManager = toolManager;
30
+ this.promptBuilder = promptBuilder || new PromptBuilder();
31
+ }
32
+ async tick({ event, ctx, state, identity, eff, memory, transitionTap }) {
33
+ if (!identity?.sessionId)
34
+ throw new Error('Agent.tick: identity.sessionId is required');
35
+ if (!eff)
36
+ throw new Error('Agent.tick: eff is required');
37
+ const machine = new AgentMachine({
38
+ workflow: this.workflow,
39
+ executionCore: runTurn,
40
+ eff,
41
+ toolManager: this.toolManager,
42
+ promptBuilder: this.promptBuilder,
43
+ memory,
44
+ input: { identity, ctx, state },
45
+ transitionTap,
46
+ });
47
+ // `send` awaits the recursive _step until the machine settles at a
48
+ // parked state (or stops). When it returns, we have the final state.
49
+ await machine.send({ event: event, identity });
50
+ return {
51
+ ctx: machine.ctx,
52
+ state: machine.state,
53
+ parked: machine.isParked,
54
+ stateTimeoutUnhandled: machine.stateTimeoutUnhandled,
55
+ metrics: { llmCalls: machine.llmCalls, tokens: machine.tokens },
56
+ };
57
+ }
58
+ }
@@ -0,0 +1,38 @@
1
+ import { query } from '@anthropic-ai/claude-agent-sdk';
2
+ import type { Workflow } from '../types.js';
3
+ import type { ToolManager } from '../tools/ToolManager.js';
4
+ import type { TickInput, TickOutput } from './Agent.js';
5
+ export type CognitionKind = 'fsm' | 'claude-sdk';
6
+ export interface ClaudeSdkAgentOptions {
7
+ workflow: Workflow;
8
+ toolManager: ToolManager;
9
+ description: string;
10
+ specialization?: string | null;
11
+ model: string;
12
+ anthropicKey: string;
13
+ operatorKey: string;
14
+ agentId: string;
15
+ maxTurns?: number;
16
+ /** Injectable for tests. Defaults to @anthropic-ai/claude-agent-sdk query(). */
17
+ queryFn?: typeof query;
18
+ }
19
+ /**
20
+ * A Ziggs agent whose brain is the Claude Agent SDK. Same host, same `eff`
21
+ * side channel, same tool surface as the FSM `Agent` — the ONLY difference is
22
+ * the loop: instead of the FSM + `runTurn`, a wake drives a headless Claude
23
+ * `query()` (see ZIG-784: "host is the product; the brain is how it decides").
24
+ */
25
+ export declare class ClaudeSdkAgent {
26
+ workflow: Workflow;
27
+ toolManager: ToolManager;
28
+ private description;
29
+ private specialization;
30
+ private model;
31
+ private anthropicKey;
32
+ private operatorKey;
33
+ private agentId;
34
+ private maxTurns;
35
+ private queryFn;
36
+ constructor(opts: ClaudeSdkAgentOptions);
37
+ tick({ event, ctx, state, identity, eff }: TickInput): Promise<TickOutput>;
38
+ }
@@ -0,0 +1,322 @@
1
+ import { query } from '@anthropic-ai/claude-agent-sdk';
2
+ import { outcomeFromEvent } from '../runtime/buildOutcome.js';
3
+ import { bootstrapCtx } from '../runtime/bootstrapCtx.js';
4
+ import { CLAUDE_SDK_PROTOCOL } from './claudeSdkProtocol.js';
5
+ import { buildZiggsSdkMcpServer } from './toolManagerToSdkMcp.js';
6
+ import { claudeQueryOptions, buildClaudeEventPrompt, summarizeContext, extractAssistantText, } from './claudeCognition.js';
7
+ import { assertHostedAgentZiggsTools } from './hostedAgentConnection.js';
8
+ import { runtimeLog } from '../shared/runtimeLog.js';
9
+ const DEFAULT_STATE = 'ready';
10
+ /**
11
+ * Per-task turn cap. A wake drives one `query()` that may loop up to this many
12
+ * internal turns; the tool schema + system prompt ride every turn, so this is
13
+ * the direct multiplier on a task's token spend. 30 let a confused MCP agent
14
+ * burn schema×30 per wake; brokered tasks (fetch → record → done) converge in a
15
+ * handful of turns, so 12 is ample. Env-overridable (raise for genuinely long
16
+ * agents). ZIG-759. */
17
+ const DEFAULT_MAX_TURNS = Number(process.env.CLAUDE_SDK_MAX_TURNS ?? 12);
18
+ /**
19
+ * Hard ceiling on billable input tokens (incl. cache read/write) a single wake
20
+ * may spend before we abort the query. Defense-in-depth on the shared fleet key
21
+ * against a runaway loop; set high enough that normal tasks never trip it. The
22
+ * task ends unfinished (and logs loudly) rather than burning unbounded. ZIG-759. */
23
+ const MAX_TASK_INPUT_TOKENS = Number(process.env.CLAUDE_SDK_MAX_INPUT_TOKENS ?? 500_000);
24
+ /**
25
+ * Per-lane wake-rate circuit breaker. The per-wake caps above bound one wake;
26
+ * 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 skips
28
+ * agent actors; host drops self-caused events), so this is a backstop for any
29
+ * future echo path: a lane waking faster than real traffic plausibly drives it
30
+ * skips the LLM (and logs loudly) instead of draining the fleet key. */
31
+ const WAKE_WINDOW_MS = 10 * 60_000;
32
+ const MAX_WAKES_PER_WINDOW = Number(process.env.CLAUDE_SDK_MAX_WAKES_PER_WINDOW ?? 40);
33
+ /**
34
+ * Resumed-transcript ceiling (ZIG-816). Each wake resumes the lane's Claude
35
+ * session, so the whole transcript (event prompts, tool results, summaries)
36
+ * replays on every turn — mostly as cache reads, but it grows without bound on
37
+ * a long-lived lane and dominates per-wake cost. When a wake's billable input
38
+ * crosses this, we drop the session id so the NEXT wake starts fresh. The
39
+ * protocol is backend-authoritative (the brain re-reads via ziggs_context_snapshot),
40
+ * so the transcript is convenience memory, not the source of truth — safe to
41
+ * discard. Env-overridable. Kept below MAX_TASK_INPUT_TOKENS so a lane resets
42
+ * before it can approach the per-wake abort. */
43
+ const MAX_SESSION_RESUME_INPUT_TOKENS = Number(process.env.CLAUDE_SDK_MAX_SESSION_INPUT_TOKENS ?? 200_000);
44
+ /** Total billable input for one API response (base + cache read + cache write). */
45
+ function billableInput(usage) {
46
+ if (!usage)
47
+ return 0;
48
+ return ((usage.input_tokens ?? 0) +
49
+ (usage.cache_read_input_tokens ?? 0) +
50
+ (usage.cache_creation_input_tokens ?? 0));
51
+ }
52
+ /**
53
+ * Per-API-call usage. Only assistant turns carry it — the result message's
54
+ * usage is the CUMULATIVE total of the whole query, so adding it on top of
55
+ * the per-turn sums would double-count the wake.
56
+ */
57
+ function usageOf(message) {
58
+ if (message.type === 'assistant') {
59
+ return message.message?.usage;
60
+ }
61
+ return undefined;
62
+ }
63
+ function extractMetrics(message) {
64
+ if (message.type !== 'result')
65
+ return null;
66
+ const usage = message.usage;
67
+ // Billable input includes cache reads/writes — that is what the Anthropic
68
+ // console counts and what the fleet key pays for. Reporting only the
69
+ // uncached slice made our telemetry read ~10-20x under the real usage.
70
+ const input = billableInput(usage);
71
+ const output = usage?.output_tokens ?? 0;
72
+ // Cache read/write are already folded into `input` above; surfaced separately
73
+ // here for per-tick visibility (metricsTap metadata / dashboards, ZIG-813).
74
+ const cacheRead = usage?.cache_read_input_tokens ?? 0;
75
+ const cacheCreation = usage?.cache_creation_input_tokens ?? 0;
76
+ return {
77
+ llmCalls: message.num_turns ?? 1,
78
+ // `input` is already the full billable input (base + cache); total = input + output.
79
+ tokens: { total: input + output, input, output, cacheRead, cacheCreation },
80
+ };
81
+ }
82
+ /**
83
+ * Resolve the agreement this wake acts under, for a delegated per-user MCP call
84
+ * (ZIG-831/832). Without it the gateway resolves the connection owner from
85
+ * `senderId` — which, under a delegation, is the delegating agent (who owns no
86
+ * connection) → a spurious NO_USER_CONNECTION even though the payer IS connected.
87
+ *
88
+ * The id must be found on ANY wake the worker might fetch on, not just
89
+ * `task_result` events. In practice the worker often fetches while reacting to
90
+ * the proposal-approved / agreement-active lifecycle events, which carry the id
91
+ * at the event top level rather than under `result`. Reading only
92
+ * `result.agreementId` (the original ZIG-831 wiring) left those wakes with no
93
+ * agreement → the false "authorize your connection" prompt. Resolve from every
94
+ * reliable source instead:
95
+ * 1. task_result events → `result.agreementId`
96
+ * 2. agreement-lifecycle events → top-level `agreementId`
97
+ * 3. task lanes → `sessionId` IS the agreement id
98
+ * 4. fallback → the single active agreement in context this agent provides
99
+ */
100
+ function resolveOnBehalfAgreementId(rawEvent, contextSnap, agentId, sessionId) {
101
+ const str = (v) => typeof v === 'string' && v ? v : undefined;
102
+ const fromResult = str(rawEvent?.result?.agreementId);
103
+ if (fromResult)
104
+ return fromResult;
105
+ const fromEvent = str(rawEvent?.agreementId);
106
+ if (fromEvent)
107
+ return fromEvent;
108
+ if (typeof sessionId === 'string' && sessionId.startsWith('agr_'))
109
+ return sessionId;
110
+ // Fallback: exactly one active agreement in context where this agent is the
111
+ // provider. Guarded to a single match so we never send the wrong agreement.
112
+ const ags = Array.isArray(contextSnap?.agreements)
113
+ ? contextSnap.agreements
114
+ : [];
115
+ const provides = ags.filter((a) => {
116
+ const p = a?.parties ?? a;
117
+ const providerIds = [p?.provider, p?.providerAgent, a?.providerId, a?.providerAgentId, a?.executorId];
118
+ const isProvider = providerIds.some((x) => x === agentId);
119
+ const status = a?.status != null ? String(a.status).toLowerCase() : '';
120
+ const active = !status || status === 'active' || status === 'approved';
121
+ return isProvider && active;
122
+ });
123
+ if (provides.length === 1) {
124
+ return str(provides[0].agreementId) ?? str(provides[0].id) ?? str(provides[0]._id);
125
+ }
126
+ return undefined;
127
+ }
128
+ /**
129
+ * A Ziggs agent whose brain is the Claude Agent SDK. Same host, same `eff`
130
+ * side channel, same tool surface as the FSM `Agent` — the ONLY difference is
131
+ * the loop: instead of the FSM + `runTurn`, a wake drives a headless Claude
132
+ * `query()` (see ZIG-784: "host is the product; the brain is how it decides").
133
+ */
134
+ export class ClaudeSdkAgent {
135
+ workflow;
136
+ toolManager;
137
+ description;
138
+ specialization;
139
+ model;
140
+ anthropicKey;
141
+ operatorKey;
142
+ agentId;
143
+ maxTurns;
144
+ queryFn;
145
+ constructor(opts) {
146
+ if (!opts.workflow?.states)
147
+ throw new Error('ClaudeSdkAgent: workflow with states is required');
148
+ if (!opts.toolManager)
149
+ throw new Error('ClaudeSdkAgent: toolManager is required');
150
+ if (!opts.anthropicKey)
151
+ throw new Error('ClaudeSdkAgent: anthropicKey is required');
152
+ this.workflow = opts.workflow;
153
+ this.toolManager = opts.toolManager;
154
+ this.description = opts.description;
155
+ this.specialization = opts.specialization ?? null;
156
+ this.model = opts.model;
157
+ this.anthropicKey = opts.anthropicKey;
158
+ this.operatorKey = opts.operatorKey;
159
+ this.agentId = opts.agentId;
160
+ this.maxTurns = opts.maxTurns ?? DEFAULT_MAX_TURNS;
161
+ this.queryFn = opts.queryFn ?? query;
162
+ }
163
+ async tick({ event, ctx, state, identity, eff }) {
164
+ if (!identity?.sessionId)
165
+ throw new Error('ClaudeSdkAgent.tick: identity.sessionId is required');
166
+ if (!eff)
167
+ throw new Error('ClaudeSdkAgent.tick: eff is required');
168
+ const rawEvent = event;
169
+ const nextCtx = bootstrapCtx(identity, ctx);
170
+ nextCtx.lastOutcome = outcomeFromEvent(rawEvent ?? null, identity.agentId);
171
+ const now = Date.now();
172
+ const recentWakes = (nextCtx.recentWakes ?? []).filter((t) => now - t < WAKE_WINDOW_MS);
173
+ if (recentWakes.length >= MAX_WAKES_PER_WINDOW) {
174
+ runtimeLog.warn('ClaudeSdkAgent', `wake-rate breaker tripped: ${recentWakes.length} wakes in ${WAKE_WINDOW_MS / 60_000}min ` +
175
+ `session=${identity.sessionId} agent=${this.agentId} — skipping LLM for this wake`);
176
+ nextCtx.recentWakes = recentWakes;
177
+ return {
178
+ ctx: nextCtx,
179
+ state: state ?? DEFAULT_STATE,
180
+ parked: true,
181
+ stateTimeoutUnhandled: false,
182
+ metrics: { llmCalls: 0, tokens: { total: 0, input: 0, output: 0 } },
183
+ };
184
+ }
185
+ nextCtx.recentWakes = [...recentWakes, now];
186
+ let contextSnap = null;
187
+ try {
188
+ contextSnap = await eff({
189
+ kind: 'read-context',
190
+ sessionId: identity.sessionId,
191
+ });
192
+ }
193
+ catch (err) {
194
+ runtimeLog.warn('ClaudeSdkAgent', `read-context failed session=${identity.sessionId}: ${err.message}`);
195
+ }
196
+ // Same guard as the FSM's runTurn: a message wake for a chat the backend
197
+ // no longer resolves (deleted / not visible) has no world to act in —
198
+ // skip the query instead of burning a wake on a fabricated empty history.
199
+ // Task lanes (sessionId = agreementId) also trip chatMissing and must run.
200
+ if (contextSnap?.chatMissing === true && nextCtx.lastOutcome?.kind === 'message-received') {
201
+ runtimeLog.warn('ClaudeSdkAgent', `chat ${identity.sessionId} not found on backend — skipping message wake agent=${this.agentId}`);
202
+ return {
203
+ ctx: nextCtx,
204
+ state: state ?? DEFAULT_STATE,
205
+ parked: true,
206
+ stateTimeoutUnhandled: false,
207
+ metrics: { llmCalls: 0, tokens: { total: 0, input: 0, output: 0 } },
208
+ };
209
+ }
210
+ const roleBlock = [
211
+ `## Your role`,
212
+ this.description,
213
+ this.specialization ? `Specialization: ${this.specialization}` : '',
214
+ ].filter(Boolean).join('\n');
215
+ const systemPrompt = `${CLAUDE_SDK_PROTOCOL}\n\n${roleBlock}`;
216
+ const runId = crypto.randomUUID();
217
+ const toolContext = {
218
+ operatorKey: this.operatorKey,
219
+ agentId: this.agentId,
220
+ chatId: identity.sessionId,
221
+ sessionId: identity.sessionId,
222
+ eff,
223
+ senderId: rawEvent?.senderId != null ? String(rawEvent.senderId) : undefined,
224
+ senderType: rawEvent?.senderType != null ? String(rawEvent.senderType) : undefined,
225
+ // ZIG-831/832: agreement this wake acts under → lets a delegated per-user
226
+ // MCP call resolve the originating end-user (payer) rather than the
227
+ // delegating agent (senderId). Resolved from every reliable wire source,
228
+ // not just task_result events — see resolveOnBehalfAgreementId.
229
+ onBehalfOfAgreementId: resolveOnBehalfAgreementId(rawEvent, contextSnap, this.agentId, identity.sessionId),
230
+ runId,
231
+ };
232
+ const availableTools = await this.toolManager.getAvailableTools();
233
+ const mcpServer = buildZiggsSdkMcpServer(availableTools, () => toolContext, {
234
+ name: 'ziggs',
235
+ instructions: 'In-process Ziggs tools backed by api-client. No external MCP process.',
236
+ });
237
+ const prompt = buildClaudeEventPrompt(rawEvent, summarizeContext(contextSnap));
238
+ const mcpServers = { ziggs: mcpServer };
239
+ assertHostedAgentZiggsTools(mcpServers);
240
+ const queryOptions = claudeQueryOptions({
241
+ systemPrompt,
242
+ model: this.model,
243
+ mcpServers,
244
+ maxTurns: this.maxTurns,
245
+ apiKey: this.anthropicKey,
246
+ stderr: (data) => {
247
+ const line = data.trim();
248
+ if (line)
249
+ runtimeLog.warn('ClaudeSdkAgent', `stderr session=${identity.sessionId}: ${line}`);
250
+ },
251
+ resume: nextCtx.claudeSessionId,
252
+ });
253
+ let metrics = { llmCalls: 0, tokens: { total: 0, input: 0, output: 0, cacheRead: 0, cacheCreation: 0 } };
254
+ let sessionId = nextCtx.claudeSessionId;
255
+ let spentInput = 0;
256
+ try {
257
+ for await (const message of this.queryFn({ prompt, options: queryOptions })) {
258
+ if ('session_id' in message && typeof message.session_id === 'string') {
259
+ sessionId = message.session_id;
260
+ }
261
+ // Surface the model's own prose so a hired Claude agent is as legible as
262
+ // an FSM agent (ZIG-787). Each completed assistant turn's text is recorded
263
+ // as a `thought` — the SAME agent-private channel the FSM uses for its
264
+ // reasoning (runTurn `emitThought` → record-event → recordThought), so it
265
+ // shows in the hired-agent view without cluttering the counterparty chat.
266
+ // Pure tool-use turns carry no text and record nothing.
267
+ //
268
+ // Fire-and-forget on purpose: this runs inside `for await (query())`, so
269
+ // awaiting an artifacts round-trip per turn would couple model-stream
270
+ // consumption to the artifacts backend — a slow/wedged backend would stall
271
+ // the whole wake, not just fail a breadcrumb. Recording is best-effort and
272
+ // thought ordering is not load-bearing, so we let it run in the background
273
+ // and swallow failures with a warn.
274
+ if (message.type === 'assistant') {
275
+ const text = extractAssistantText(message).trim();
276
+ if (text) {
277
+ void eff({ kind: 'record-event', sessionId: identity.sessionId, entry: { kind: 'thought', text } })
278
+ .catch((err) => runtimeLog.warn('ClaudeSdkAgent', `record thought failed session=${identity.sessionId}: ${err.message}`));
279
+ }
280
+ }
281
+ const m = extractMetrics(message);
282
+ if (m)
283
+ metrics = m;
284
+ if (message.type === 'result' && message.subtype !== 'success') {
285
+ runtimeLog.warn('ClaudeSdkAgent', `query ended without success subtype=${message.subtype} session=${identity.sessionId}`);
286
+ }
287
+ // Runaway guard (ZIG-759): abort before an unbounded loop drains the
288
+ // shared fleet key. Breaking the for-await calls the generator's return(),
289
+ // which terminates the underlying query.
290
+ spentInput += billableInput(usageOf(message));
291
+ if (spentInput > MAX_TASK_INPUT_TOKENS) {
292
+ runtimeLog.warn('ClaudeSdkAgent', `input-token budget exceeded (~${spentInput} > ${MAX_TASK_INPUT_TOKENS}) session=${identity.sessionId} agent=${this.agentId} — aborting query to cap spend (ZIG-759)`);
293
+ break;
294
+ }
295
+ }
296
+ }
297
+ catch (err) {
298
+ runtimeLog.warn('ClaudeSdkAgent', `query failed session=${identity.sessionId}: ${err.message}`);
299
+ throw err;
300
+ }
301
+ if (sessionId)
302
+ nextCtx.claudeSessionId = sessionId;
303
+ // ZIG-816: bound resumed-transcript growth. `metrics.tokens.input` is the
304
+ // wake's billable input off the result message (dominated by the resumed
305
+ // transcript's cache reads); `spentInput` covers an aborted wake that never
306
+ // produced a result. Past the ceiling, forget the session so the next wake
307
+ // starts fresh rather than paying an ever-larger replay.
308
+ const wakeInput = Math.max(metrics.tokens.input, spentInput);
309
+ if (nextCtx.claudeSessionId && wakeInput > MAX_SESSION_RESUME_INPUT_TOKENS) {
310
+ runtimeLog.info('ClaudeSdkAgent', `resetting Claude session (wake input ~${wakeInput} > ${MAX_SESSION_RESUME_INPUT_TOKENS}) ` +
311
+ `session=${identity.sessionId} agent=${this.agentId} — next wake starts fresh (ZIG-816)`);
312
+ nextCtx.claudeSessionId = undefined;
313
+ }
314
+ return {
315
+ ctx: nextCtx,
316
+ state: state ?? DEFAULT_STATE,
317
+ parked: true,
318
+ stateTimeoutUnhandled: false,
319
+ metrics,
320
+ };
321
+ }
322
+ }
@@ -0,0 +1,67 @@
1
+ /**
2
+ * Shared Claude-Agent-SDK cognition pieces.
3
+ *
4
+ * Everything a Claude-brain agent needs to turn a Ziggs wake into a `query()`
5
+ * call, EXCEPT the loop itself. `ClaudeSdkAgent` owns the loop (drive `query()`,
6
+ * collect metrics); this module owns the periphery so it lives in one place and
7
+ * is unit-testable without spinning up the SDK.
8
+ *
9
+ * The lockdown option set (`tools: []`, `strictMcpConfig`, `settingSources: []`,
10
+ * bypass permissions, no local settings) is a security boundary, not a default —
11
+ * it MUST be applied on every Claude-brain wake. Centralizing it here means a
12
+ * caller cannot silently drift from it.
13
+ */
14
+ import type { Options, SDKAssistantMessage } from '@anthropic-ai/claude-agent-sdk';
15
+ import type { RawEvent } from '../runtime/buildOutcome.js';
16
+ export interface ClaudeQueryOptionsParams {
17
+ /** Protocol + role system prompt. */
18
+ systemPrompt: string;
19
+ /** Claude model id for the brain. */
20
+ model: string;
21
+ /** In-process MCP servers exposing the agent's tools (e.g. `{ ziggs }`). */
22
+ mcpServers: NonNullable<Options['mcpServers']>;
23
+ /** Max agentic turns per wake. */
24
+ maxTurns: number;
25
+ /** Working dir for the query subprocess. Defaults to `$CLAUDE_SDK_CWD` or `/tmp`. */
26
+ cwd?: string;
27
+ /** stderr sink for the query subprocess. */
28
+ stderr?: (data: string) => void;
29
+ /** Claude session id to resume across ticks. Omitted → a fresh session. */
30
+ resume?: string;
31
+ /**
32
+ * Anthropic API key for this wake. Passed to the query subprocess via the
33
+ * `env` option (which inherits `process.env` and overrides the key) rather
34
+ * than mutating the parent process's global `process.env` — so agents with
35
+ * different keys can share one process safely.
36
+ */
37
+ apiKey?: string;
38
+ }
39
+ /**
40
+ * Pick the Claude model for a Claude-brain wake. The brain must run a `claude-*`
41
+ * model, but the host's default model is provider-agnostic (an OpenAI id today),
42
+ * so this prefers an explicit `claude-*` id (direct or via `services.llm`) and
43
+ * otherwise falls back to a safe Claude default — never an OpenAI id.
44
+ */
45
+ export declare function resolveClaudeModel(preferred?: string, servicesModel?: string): string;
46
+ /**
47
+ * Build the `query()` options for a Claude-brain wake. The lockdown fields are
48
+ * fixed here; callers only supply the per-wake variables (prompt, model, tools,
49
+ * turn budget, resume). Returns `Options` so it drops straight into `query()`.
50
+ */
51
+ export declare function claudeQueryOptions(params: ClaudeQueryOptionsParams): Options;
52
+ /**
53
+ * Concatenate the text blocks of a completed Claude assistant turn. Tool-use,
54
+ * thinking, and other block kinds are skipped — only the model's user-facing
55
+ * prose is returned (empty string for a pure tool-use turn). This is what a
56
+ * Claude-brain wake records as a `thought` per turn (ZIG-787), the same way
57
+ * the FSM's `emitThought` surfaces the model's reasoning.
58
+ */
59
+ export declare function extractAssistantText(message: SDKAssistantMessage): string;
60
+ /** One-line digest of a context snapshot for the wake prompt. */
61
+ export declare function summarizeContext(snap: Record<string, unknown> | null | undefined): string;
62
+ /**
63
+ * Turn a Ziggs wake event + context digest into the user prompt that starts a
64
+ * Claude-brain turn. The agent's standing instructions come from its system
65
+ * prompt (protocol + role); this is just "here's what woke you, go handle it."
66
+ */
67
+ export declare function buildClaudeEventPrompt(event: RawEvent | undefined, contextSummary: string): string;
@@ -0,0 +1,103 @@
1
+ const DEFAULT_CLAUDE_MODEL = 'claude-sonnet-4-6';
2
+ /**
3
+ * Pick the Claude model for a Claude-brain wake. The brain must run a `claude-*`
4
+ * model, but the host's default model is provider-agnostic (an OpenAI id today),
5
+ * so this prefers an explicit `claude-*` id (direct or via `services.llm`) and
6
+ * otherwise falls back to a safe Claude default — never an OpenAI id.
7
+ */
8
+ export function resolveClaudeModel(preferred, servicesModel) {
9
+ if (preferred?.startsWith('claude'))
10
+ return preferred;
11
+ if (servicesModel?.startsWith('claude'))
12
+ return servicesModel;
13
+ return DEFAULT_CLAUDE_MODEL;
14
+ }
15
+ /**
16
+ * Build the `query()` options for a Claude-brain wake. The lockdown fields are
17
+ * fixed here; callers only supply the per-wake variables (prompt, model, tools,
18
+ * turn budget, resume). Returns `Options` so it drops straight into `query()`.
19
+ */
20
+ export function claudeQueryOptions(params) {
21
+ return {
22
+ systemPrompt: params.systemPrompt,
23
+ model: params.model,
24
+ mcpServers: params.mcpServers,
25
+ // The agent's ONLY capabilities are its MCP tools — no built-in file/bash/web
26
+ // tools, no project/user/plugin MCP config, no local settings / CLAUDE.md.
27
+ tools: [],
28
+ strictMcpConfig: true,
29
+ settingSources: [],
30
+ permissionMode: 'bypassPermissions',
31
+ allowDangerouslySkipPermissions: true,
32
+ maxTurns: params.maxTurns,
33
+ cwd: params.cwd ?? process.env.CLAUDE_SDK_CWD ?? '/tmp',
34
+ // Set the key for the subprocess without touching the parent process.env.
35
+ // `env` replaces the subprocess environment, so spread process.env to keep
36
+ // PATH/HOME/etc. and override only the key.
37
+ ...(params.apiKey ? { env: { ...process.env, ANTHROPIC_API_KEY: params.apiKey } } : {}),
38
+ ...(params.stderr ? { stderr: params.stderr } : {}),
39
+ ...(params.resume ? { resume: params.resume } : {}),
40
+ };
41
+ }
42
+ /**
43
+ * Concatenate the text blocks of a completed Claude assistant turn. Tool-use,
44
+ * thinking, and other block kinds are skipped — only the model's user-facing
45
+ * prose is returned (empty string for a pure tool-use turn). This is what a
46
+ * Claude-brain wake records as a `thought` per turn (ZIG-787), the same way
47
+ * the FSM's `emitThought` surfaces the model's reasoning.
48
+ */
49
+ export function extractAssistantText(message) {
50
+ const content = message.message?.content;
51
+ if (!Array.isArray(content))
52
+ return '';
53
+ let out = '';
54
+ for (const block of content) {
55
+ if (block && block.type === 'text') {
56
+ out += block.text ?? '';
57
+ }
58
+ }
59
+ return out;
60
+ }
61
+ /** One-line digest of a context snapshot for the wake prompt. */
62
+ export function summarizeContext(snap) {
63
+ if (!snap)
64
+ return '';
65
+ const agreements = Array.isArray(snap.agreements) ? snap.agreements.length : 0;
66
+ const history = Array.isArray(snap.history) ? snap.history.length : 0;
67
+ const users = Array.isArray(snap.users) ? snap.users.length : 0;
68
+ return `history=${history} messages, agreements=${agreements}, users=${users}`;
69
+ }
70
+ /**
71
+ * Turn a Ziggs wake event + context digest into the user prompt that starts a
72
+ * Claude-brain turn. The agent's standing instructions come from its system
73
+ * prompt (protocol + role); this is just "here's what woke you, go handle it."
74
+ */
75
+ export function buildClaudeEventPrompt(event, contextSummary) {
76
+ const lines = ['You received a new Ziggs event. Handle it per your protocol.'];
77
+ if (event?.type)
78
+ lines.push(`Event type: ${event.type}`);
79
+ if (event?.text)
80
+ lines.push(`Message: ${event.text}`);
81
+ if (event?.senderId)
82
+ lines.push(`Sender: ${event.senderId}`);
83
+ if (event?.operation)
84
+ lines.push(`Operation: ${event.operation}`);
85
+ // ZIG-832: surface the task/agreement this event carries. Without it the brain
86
+ // can't tell WHICH task to update/complete (workers otherwise stall asking for
87
+ // their own taskId) and can't see whether the referenced step already ran or
88
+ // failed — which makes an orchestrator re-spawn duplicate delegations.
89
+ const result = event?.result;
90
+ if (result && typeof result === 'object') {
91
+ if (typeof result.taskId === 'string')
92
+ lines.push(`Task: ${result.taskId}`);
93
+ if (typeof result.agreementId === 'string')
94
+ lines.push(`Agreement: ${result.agreementId}`);
95
+ if (typeof result.state === 'string')
96
+ lines.push(`Task state: ${result.state}`);
97
+ }
98
+ if (contextSummary) {
99
+ lines.push('', 'Session context snapshot:', contextSummary);
100
+ }
101
+ lines.push('', 'Use ziggs_context_snapshot if you need more context. If the event needs nothing from you, say so briefly and stop.');
102
+ return lines.join('\n');
103
+ }