@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
package/src/AgentHost.ts DELETED
@@ -1,814 +0,0 @@
1
- import type { AgentConfig, Ctx, EffectHandler, Identity, Workflow } from './types.js';
2
- import { Agent } from './agent/Agent.js';
3
- import { ClaudeSdkAgent, type CognitionKind } from './agent/ClaudeSdkAgent.js';
4
- import { CLAUDE_SDK_TOOLS } from './agent/claudeSdkTools.js';
5
- import { resolveClaudeModel } from './agent/claudeCognition.js';
6
-
7
- export type { CognitionKind };
8
- import { EventQueue } from './server/EventQueue.js';
9
- import { StateTimers } from './server/StateTimers.js';
10
- import { createZiggsEffectHandler } from './server/ZiggsEffectHandler.js';
11
- import { normalizeIncomingEvent, type NormalizedEvent } from './ingress/normalizeIncoming.js';
12
- import { PromptBuilder } from './runtime/PromptBuilder.js';
13
- import { InMemoryStore, type MemoryStore } from './memory/MemoryStore.js';
14
-
15
- /**
16
- * Operator-provided FSM session persistence.
17
- *
18
- * AgentHost calls load() before each tick when a lane has no in-memory state
19
- * (e.g. after a container restart) and save() after every successful tick.
20
- * A null return from load() means "no prior state — start fresh."
21
- */
22
- export interface SessionStore {
23
- load(laneKey: string): Promise<{ ctx?: Ctx; state?: string } | null | undefined>;
24
- save(laneKey: string, session: { ctx?: Ctx; state?: string }): Promise<void>;
25
- }
26
- import { ToolManager } from './tools/ToolManager.js';
27
- import type { ToolDefinition } from './tools/defineTool.js';
28
- import { PROTOCOL_TOOLS } from './server/tools/tier1/protocolTools.js';
29
- import { OpenAIAdapter } from './adapters/OpenAIAdapter.js';
30
- import { AnthropicAdapter } from './adapters/AnthropicAdapter.js';
31
- import {
32
- WebSocketClient,
33
- ArtifactsClient,
34
- ContextReadClient,
35
- TelemetryClient,
36
- InboxClient,
37
- type MessageMetadata,
38
- } from '@ziggs-ai/api-client';
39
- import { TaskService } from './server/tasks/TaskService.js';
40
- import { AgreementService } from './server/agreements/AgreementService.js';
41
- import { runInboxCatchUp } from './server/InboxCatchUp.js';
42
- import { SeenMessages } from './server/SeenMessages.js';
43
- import { runtimeLog } from './shared/runtimeLog.js';
44
-
45
- /**
46
- * The single fleet-wide default model — the one place the fallback lives.
47
- * Change this (or set `LLM_MODEL`) to move the whole fleet; nothing hardcodes a
48
- * model anywhere else. The id also picks the provider: a `claude-*` value routes
49
- * to AnthropicAdapter (+ ANTHROPIC_API_KEY), anything else to OpenAIAdapter — see
50
- * the `model?.startsWith('claude')` branch in createBackendServices below.
51
- */
52
- export const DEFAULT_MODEL = 'claude-haiku-4-5';
53
-
54
- // ConnectionPool lives in a separate module; resolved via a computed-path
55
- // dynamic import to avoid a circular import with AgentHost.
56
- async function loadConnectionPool(): Promise<new (opts?: { maxActive?: number; idleTimeoutMs?: number }) => unknown> {
57
- const modPath = './server/ConnectionPool.js';
58
- const mod = await import(/* @vite-ignore */ modPath) as { ConnectionPool: new (opts?: { maxActive?: number; idleTimeoutMs?: number }) => unknown };
59
- return mod.ConnectionPool;
60
- }
61
-
62
- /**
63
- * Given a normalized event, return the sessionId for this agent. Return
64
- * `null` to skip processing. Lets each Agent define what "session" means —
65
- * chat-scoped agents bucket by chatId (the default), task-scoped agents
66
- * could bucket by taskId, cron agents by schedule id, etc.
67
- */
68
- export type SessionResolver = (normalized: NormalizedEvent, ownAgentId: string) => string | null;
69
-
70
- const DEFAULT_SESSION_RESOLVER: SessionResolver = (normalized) => normalized.chatId;
71
-
72
- interface LlmAdapter {
73
- chatMessages(messages: unknown[], tools?: unknown[], options?: Record<string, unknown>): Promise<unknown>;
74
- }
75
-
76
- interface AgentServices {
77
- tools?: ToolDefinition[];
78
- llm?: { model?: string } | LlmAdapter;
79
- [key: string]: unknown;
80
- }
81
-
82
-
83
- export interface AgentHostOptions {
84
- openaiKey?: string;
85
- anthropicKey?: string;
86
- model?: string;
87
- operatorKey?: string;
88
- agentId?: string;
89
- name?: string | null;
90
- wsUrl?: string;
91
- description?: string;
92
- specialization?: string | null;
93
- tools?: ToolDefinition[];
94
- taskTools?: 'all' | 'none' | string[];
95
- /** Cognition runtime: FSM (default) or Claude Agent SDK headless delegate. */
96
- cognition?: CognitionKind;
97
- /** Max agentic turns per Claude SDK wake. Default 30. */
98
- claudeMaxTurns?: number;
99
- services?: AgentServices;
100
- workflow?: Workflow;
101
- // Convenience: states + initial without an explicit workflow.
102
- states?: Workflow['states'];
103
- initial?: string;
104
- tickTimeoutMs?: number;
105
- /** Per-agent session keying. Defaults to `(n) => n.chatId`. */
106
- sessionResolver?: SessionResolver;
107
- /**
108
- * Agent-private long-term memory. Operator-provided. Default: a fresh
109
- * `InMemoryStore` (non-durable; lost on process restart). For durability,
110
- * pass `new FileMemoryStore(path)` or your own implementation.
111
- */
112
- memory?: MemoryStore;
113
- /**
114
- * FSM session persistence. Operator-provided. When set, AgentHost loads
115
- * prior state before each tick (cold-start recovery) and saves after every
116
- * successful tick. Omit to keep state in-process only (default behaviour).
117
- */
118
- sessionStore?: SessionStore;
119
- /** Observability tap: called after every effect with the effect and its response. */
120
- effectTap?: (effect: import('./types.js').Effect, response: unknown) => void;
121
- /** Observability tap: called on every FSM state transition, no-match, depth-guard, or error-escape (ZIG-442). */
122
- transitionTap?: (event: import('./runtime/AgentMachine.js').TransitionEvent) => void;
123
- /**
124
- * Usage/cost tap: called once per settled tick with the tick's authoritative
125
- * token usage (result.metrics). This is the SINGLE source of truth for
126
- * usage/cost across every cognition — the FSM (llm-call + streamed reply +
127
- * tool-internal LLM) and the Claude brain (incl. cache tokens) both funnel
128
- * their spend here (ZIG-813). effectTap / tool generations are structural
129
- * tracing only and must NOT also carry billable usage, or cost double-counts.
130
- */
131
- metricsTap?: (info: {
132
- agentId: string;
133
- sessionId: string;
134
- model: string;
135
- cognition: string;
136
- metrics: import('./agent/Agent.js').TickOutput['metrics'];
137
- }) => void;
138
- /** Production monitoring ingest (ZIG-360). Default on; set false to disable. */
139
- telemetryIngest?: boolean;
140
- /**
141
- * Pull inbox on every WS connect/reconnect (ZIG-454). Default true.
142
- * Recovers messages lost when backend in-memory pending queues are dropped.
143
- */
144
- inboxCatchUp?: boolean;
145
- }
146
-
147
- function createToolManager(options: AgentHostOptions): ToolManager {
148
- const toolManager = new ToolManager();
149
- const userTools = options.tools || options.services?.tools || [];
150
- let taskTools: ToolDefinition[];
151
- if (options.taskTools === 'none') taskTools = [];
152
- else if (Array.isArray(options.taskTools)) taskTools = PROTOCOL_TOOLS.filter((t: ToolDefinition) => (options.taskTools as string[]).includes(t.schema.function.name as string));
153
- else taskTools = PROTOCOL_TOOLS; // 'all' or undefined
154
- const claudeSdkTools = options.cognition === 'claude-sdk' ? CLAUDE_SDK_TOOLS : [];
155
- toolManager.registerAll([...taskTools, ...claudeSdkTools, ...(Array.isArray(userTools) ? userTools : [])]);
156
- return toolManager;
157
- }
158
-
159
- function isLlmAdapter(v: unknown): v is LlmAdapter {
160
- return typeof v === 'object' && v !== null && typeof (v as LlmAdapter).chatMessages === 'function';
161
- }
162
-
163
- function createBackendServices(options: AgentHostOptions) {
164
- const toolManager = createToolManager(options);
165
- const model = (options.services?.llm as { model?: string } | undefined)?.model || options.model;
166
- // The Claude brain drives query() directly and never emits llm-call/stream-text
167
- // effects, so it needs no adapter. Building one here would be dead — and worse,
168
- // the provider client throws at construction on a missing key, so a claude-sdk
169
- // agent left on the default (OpenAI) model with no openaiKey would fail to
170
- // start over an adapter it never uses. Skip it.
171
- const llm = options.cognition === 'claude-sdk'
172
- ? undefined
173
- : isLlmAdapter(options.services?.llm)
174
- ? options.services!.llm as LlmAdapter
175
- : model?.startsWith('claude')
176
- ? new AnthropicAdapter({ key: options.anthropicKey, model })
177
- : new OpenAIAdapter({ key: options.openaiKey, model });
178
- const operatorKey = options.operatorKey!;
179
- const agentId = options.agentId!;
180
- const artifactsClient = new ArtifactsClient(operatorKey, agentId);
181
- const contextReadClient = new ContextReadClient(operatorKey, agentId);
182
- const promptBuilder = new PromptBuilder({ description: options.description, specialization: options.specialization });
183
- const taskService = new TaskService(operatorKey, agentId);
184
- const agreementService = new AgreementService(operatorKey, agentId);
185
- const telemetryClient = new TelemetryClient(operatorKey, agentId);
186
- return {
187
- toolManager,
188
- llm,
189
- artifactsClient,
190
- contextReadClient,
191
- promptBuilder,
192
- taskService,
193
- agreementService,
194
- telemetryClient,
195
- };
196
- }
197
-
198
- interface LaneSession {
199
- sessionId: string;
200
- ownAgentId: string;
201
- ctx?: Ctx;
202
- state?: string;
203
- }
204
-
205
- /**
206
- * AgentHost — the Server. Owns a pure {@link Agent}, an {@link EffectHandler},
207
- * a transport (Ziggs WebSocket), and the lane router (per-session FIFO queue
208
- * + in-memory session state).
209
- *
210
- * transport (WS) → handleMessage → agent.tick(... , eff) → ...
211
- */
212
- export class AgentHost {
213
- options: Required<Pick<AgentHostOptions, 'agentId' | 'operatorKey' | 'openaiKey'>> & AgentHostOptions;
214
- toolManager!: ToolManager;
215
- // Retained for external inspection. Undefined for the Claude brain, which
216
- // drives query() directly and needs no LLM adapter.
217
- llm?: OpenAIAdapter | LlmAdapter;
218
- agent!: Agent | ClaudeSdkAgent;
219
- wsClient!: WebSocketClient;
220
- private eff!: EffectHandler;
221
- private eventQueue!: EventQueue;
222
- private sessions = new Map<string, LaneSession>();
223
- private sessionResolver: SessionResolver;
224
- private tickTimeoutMs: number;
225
- private memory: MemoryStore;
226
- private sessionStore: SessionStore | null;
227
- private taskService!: TaskService;
228
- private agreementService!: AgreementService;
229
- private messageSender!: (message: string, receiverId: string, sessionId: string) => Promise<unknown>;
230
- private seenMessages = new SeenMessages();
231
- /** Lazy inbox client for advancing the watermark on the live-push path (ZIG-832 P4). */
232
- private _inbox?: InboxClient;
233
- private stateTimers!: StateTimers;
234
- private catchUpInFlight: Promise<void> | null = null;
235
- private inboxCatchUpEnabled: boolean;
236
-
237
- constructor(options: AgentHostOptions = {}) {
238
- this.options = {
239
- openaiKey: options.openaiKey || process.env.OPENAI_API_KEY,
240
- anthropicKey: options.anthropicKey || process.env.ANTHROPIC_API_KEY,
241
- model: options.model || (!isLlmAdapter(options.services?.llm) ? options.services?.llm?.model : undefined) || DEFAULT_MODEL,
242
- operatorKey: options.operatorKey || process.env.ZIGGS_OPERATOR_KEY || undefined,
243
- agentId: options.agentId || '',
244
- name: options.name || null,
245
- wsUrl: options.wsUrl,
246
- description: options.description || 'A helpful assistant',
247
- specialization: options.specialization ?? null,
248
- tools: options.tools || [],
249
- taskTools: options.taskTools,
250
- states: options.states,
251
- initial: options.initial,
252
- services: options.services,
253
- workflow: options.workflow,
254
- tickTimeoutMs: options.tickTimeoutMs,
255
- sessionResolver: options.sessionResolver,
256
- effectTap: options.effectTap,
257
- transitionTap: options.transitionTap,
258
- metricsTap: options.metricsTap,
259
- inboxCatchUp: options.inboxCatchUp,
260
- cognition: options.cognition ?? 'fsm',
261
- claudeMaxTurns: options.claudeMaxTurns,
262
- } as Required<Pick<AgentHostOptions, 'agentId' | 'operatorKey' | 'openaiKey'>> & AgentHostOptions;
263
- this.inboxCatchUpEnabled = options.inboxCatchUp !== false;
264
- this.sessionResolver = options.sessionResolver || DEFAULT_SESSION_RESOLVER;
265
- this.tickTimeoutMs = options.tickTimeoutMs ?? 120_000;
266
- this.memory = options.memory ?? new InMemoryStore();
267
- this.sessionStore = options.sessionStore ?? null;
268
- this._validate();
269
- this._setup();
270
- }
271
-
272
- private _validate(): void {
273
- if (this.options.cognition === 'claude-sdk') {
274
- if (!this.options.anthropicKey) {
275
- throw new Error('anthropicKey is required for claude-sdk cognition — pass { anthropicKey } or set ANTHROPIC_API_KEY');
276
- }
277
- } else if (!isLlmAdapter(this.options.services?.llm)) {
278
- if (this.options.model?.startsWith('claude')) {
279
- if (!this.options.anthropicKey) throw new Error('anthropicKey is required for claude-* models — pass { anthropicKey } or set ANTHROPIC_API_KEY');
280
- } else if (!this.options.openaiKey) {
281
- throw new Error('openaiKey is required');
282
- }
283
- }
284
- if (!this.options.operatorKey) throw new Error('operatorKey is required — pass { operatorKey } or set ZIGGS_OPERATOR_KEY');
285
- if (!this.options.agentId) throw new Error('agentId is required');
286
- }
287
-
288
- private _setup(): void {
289
- const services = createBackendServices(this.options);
290
- this.toolManager = services.toolManager;
291
- this.llm = services.llm;
292
-
293
- this.wsClient = new WebSocketClient({
294
- wsUrl: this.options.wsUrl,
295
- operatorKey: this.options.operatorKey,
296
- agentId: this.options.agentId,
297
- label: this.options.name || 'agent',
298
- });
299
-
300
- const messageSender = (message: string, receiverId: string, sessionId: string, messageId?: string): Promise<unknown> => {
301
- this.wsClient.send(sessionId, receiverId, message, messageId ? { messageId } : undefined);
302
- return Promise.resolve(undefined);
303
- };
304
- const chunkSender = (sessionId: string, receiverId: string, text: string, messageId: string): void => {
305
- this.wsClient.send(sessionId, receiverId, text, { partial: true, messageId });
306
- };
307
- this.taskService = services.taskService;
308
- this.agreementService = services.agreementService;
309
- this.messageSender = messageSender;
310
-
311
- let workflow = this.options.workflow;
312
- if (!workflow && this.options.states) {
313
- workflow = {
314
- id: this.options.agentId,
315
- description: this.options.description!,
316
- initial: this.options.initial || 'idle',
317
- states: this.options.states,
318
- };
319
- }
320
- if (!workflow?.states) {
321
- // The Claude brain has no FSM — it drives query() per wake and parks. It
322
- // still plugs into the host's lane/timer machinery via a trivial parked
323
- // workflow, synthesized here so callers don't hand-write a dummy one.
324
- if (this.options.cognition === 'claude-sdk') {
325
- workflow = {
326
- id: this.options.agentId,
327
- description: this.options.description!,
328
- initial: 'ready',
329
- states: { ready: { kind: 'parked', transitions: [] } },
330
- };
331
- } else {
332
- throw new Error('AgentHost: workflow (or states + initial) is required');
333
- }
334
- }
335
-
336
- this.eff = createZiggsEffectHandler({
337
- llm: services.llm as unknown as import('./server/ZiggsEffectHandler.js').ZiggsEffectDeps['llm'],
338
- artifactsClient: services.artifactsClient,
339
- contextReadClient: services.contextReadClient,
340
- taskService: services.taskService,
341
- agreementService: services.agreementService,
342
- messageSender,
343
- chunkSender,
344
- toolManager: services.toolManager,
345
- operatorKey: this.options.operatorKey!,
346
- agentId: this.options.agentId!,
347
- telemetryClient: services.telemetryClient,
348
- telemetryIngest: this.options.telemetryIngest,
349
- effectTap: this.options.effectTap,
350
- });
351
-
352
- this.agent = this.options.cognition === 'claude-sdk'
353
- ? new ClaudeSdkAgent({
354
- workflow,
355
- toolManager: services.toolManager,
356
- description: this.options.description!,
357
- specialization: this.options.specialization,
358
- model: resolveClaudeModel(
359
- this.options.model,
360
- (this.options.services?.llm as { model?: string } | undefined)?.model,
361
- ),
362
- anthropicKey: this.options.anthropicKey!,
363
- operatorKey: this.options.operatorKey!,
364
- agentId: this.options.agentId!,
365
- // No taskService/agreementService: Claude-brain tools execute through
366
- // eff({ kind: 'tool-call' }) → the shared executor, which sources those
367
- // services from the effect handler's deps like the FSM path (ZIG-775).
368
- maxTurns: this.options.claudeMaxTurns,
369
- })
370
- : new Agent({
371
- workflow,
372
- toolManager: services.toolManager,
373
- promptBuilder: services.promptBuilder,
374
- });
375
-
376
- this.eventQueue = new EventQueue((event, laneKey) => this._processEvent(event, laneKey));
377
- this.stateTimers = new StateTimers((ev, laneKey) => { void this.eventQueue.enqueue(ev, laneKey); });
378
-
379
- this.wsClient.setMessageHandler((text: string, metadata: MessageMetadata) => this.handleMessage(text, metadata as unknown as Record<string, unknown>));
380
- this.wsClient.setResourceEventHandler((event) => this.handleResourceEvent(event as unknown as Record<string, unknown>));
381
-
382
- if (this.inboxCatchUpEnabled) {
383
- this.wsClient.onConnect(() => {
384
- void this._ensureCatchUp();
385
- });
386
- }
387
- }
388
-
389
- /**
390
- * Coalesce concurrent connect/reconnect bursts into one catch-up pass.
391
- */
392
- private _ensureCatchUp(): Promise<void> {
393
- if (!this.catchUpInFlight) {
394
- this.catchUpInFlight = this._runInboxCatchUp().finally(() => {
395
- this.catchUpInFlight = null;
396
- });
397
- }
398
- return this.catchUpInFlight;
399
- }
400
-
401
- private async _runInboxCatchUp(): Promise<void> {
402
- const label = this.options.name || 'agent';
403
- try {
404
- await runInboxCatchUp({
405
- creds: {
406
- operatorKey: this.options.operatorKey!,
407
- agentId: this.options.agentId!,
408
- },
409
- ownAgentId: this.options.agentId!,
410
- baseUrl: this.options.wsUrl ? undefined : undefined,
411
- seen: this.seenMessages,
412
- label,
413
- onMessage: (text, metadata) => this.handleMessage(text, metadata),
414
- onResourceEvent: (event) => this.handleResourceEvent(event),
415
- });
416
- } catch (err: unknown) {
417
- runtimeLog.warn(
418
- label,
419
- `inbox catch-up failed: ${(err as Error)?.message ?? err}`,
420
- );
421
- }
422
- }
423
-
424
- /**
425
- * Push notification handler — invoked when the backend sees a relevant
426
- * resource change (artifact created, task state transition, agreement
427
- * updated). The agent decides which session/lane to wake based on the
428
- * event payload; it pulls the actual data via the primitive endpoints
429
- * from inside the tick.
430
- *
431
- * Lightweight: defers all work to a tick by enqueueing a synthetic event
432
- * carrying just enough context for the agent to route it.
433
- */
434
- async handleResourceEvent(event: Record<string, unknown>): Promise<void> {
435
- if (!event || typeof event !== 'object') return;
436
- const ownAgentId = this.options.agentId;
437
- if (!ownAgentId) return;
438
-
439
- // The agent's own writes aren't news to it (same rule the backend inbox
440
- // applies to counts). The backend already skips agent actors on emit;
441
- // this guards the same invariant host-side so a self-caused event can
442
- // never re-wake the brain — that echo was a token-burning wake loop for
443
- // LLM cognitions (one wake's tool calls seeded the next wake, forever).
444
- if (event.actorId != null && String(event.actorId) === ownAgentId) {
445
- runtimeLog.debug('AgentHost', `skip self-caused resource event kind=${event.kind ?? 'unknown'} resourceId=${event.resourceId ?? 'null'}`);
446
- return;
447
- }
448
-
449
- // A `message` resource event is the second channel for news the message
450
- // push already delivered — the backend emits BOTH a `chat:message` frame
451
- // and a `resource_changed {kind:'message', resourceId: messageId}` for
452
- // every send. The resource frame exists for offline recovery (queue +
453
- // launcher wake); when we already processed that messageId live via
454
- // handleMessage, waking again just re-runs the brain over handled news
455
- // (every inbound chat message cost a second wake). Unseen ids still wake:
456
- // that's the recovery path working.
457
- if (
458
- event.kind === 'message' &&
459
- typeof event.resourceId === 'string' &&
460
- this.seenMessages.has(event.resourceId)
461
- ) {
462
- runtimeLog.debug('AgentHost', `skip resource event for already-handled message ${event.resourceId}`);
463
- return;
464
- }
465
-
466
- // Map the event to a candidate session key using the configured resolver
467
- // on a synthetic normalized-event shape. Default resolver buckets by
468
- // chatId; agents using a different sessionResolver (agreementId, taskId,
469
- // counterpartyId) will get the right lane here as long as the wire
470
- // event carries the corresponding id.
471
- const fakeNormalized = {
472
- chatId: event.chatId as string | null,
473
- event: { type: 'message' as const, timestamp: Date.now(), text: '' },
474
- shouldProcess: true,
475
- reason: null,
476
- } as NormalizedEvent;
477
- const sessionId = this.sessionResolver(fakeNormalized, ownAgentId);
478
- if (!sessionId) return;
479
-
480
- const laneKey = `${ownAgentId}:${sessionId}`;
481
- if (!this.sessions.has(laneKey)) {
482
- this.sessions.set(laneKey, { sessionId, ownAgentId });
483
- }
484
- return this.eventQueue.enqueue({ type: 'resource_changed', resource: event, chatId: event.chatId, taskId: event.taskId, agreementId: event.agreementId }, laneKey);
485
- }
486
-
487
- /** Wire entry. Normalize → resolve session → enqueue on the right lane. */
488
- async handleMessage(text: string, metadata: Record<string, unknown> = {}): Promise<void> {
489
- const messageId = metadata.messageId as string | undefined;
490
- if (messageId && this.seenMessages.has(messageId)) {
491
- runtimeLog.debug('AgentHost', `skip duplicate messageId=${messageId}`);
492
- return;
493
- }
494
- if (messageId) this.seenMessages.mark(messageId);
495
-
496
- const ownAgentId = this._resolveOwnAgentId(metadata);
497
- if (!ownAgentId) {
498
- runtimeLog.warn('AgentHost', 'handleMessage: ownAgentId missing — dropping event');
499
- return;
500
- }
501
-
502
- let enrichedText = typeof text === 'string' ? text : '';
503
- if (!enrichedText.trim()) {
504
- const agreement = metadata.agreement as Record<string, unknown> | undefined;
505
- const terms = agreement?.terms as { description?: string } | undefined;
506
- if (terms?.description) {
507
- enrichedText = String(terms.description);
508
- }
509
- }
510
-
511
- const normalized = normalizeIncomingEvent({ text: enrichedText, metadata, ownAgentId });
512
- if (!normalized.shouldProcess) {
513
- const receiverId = (metadata.receiver as Record<string, unknown> | undefined)?.id ?? metadata.receiverId;
514
- runtimeLog.warn('AgentHost', `skip reason=${normalized.reason || 'not_relevant'} ownAgentId=${ownAgentId} receiverId=${receiverId ?? 'null'} chatId=${normalized.chatId ?? 'null'}`);
515
- return;
516
- }
517
-
518
- const sessionId = this.sessionResolver(normalized, ownAgentId);
519
- if (!sessionId) {
520
- runtimeLog.warn('AgentHost', `session resolver returned null ownAgentId=${ownAgentId} chatId=${normalized.chatId ?? 'null'}`);
521
- return;
522
- }
523
-
524
- const laneKey = `${ownAgentId}:${sessionId}`;
525
- if (!this.sessions.has(laneKey)) {
526
- this.sessions.set(laneKey, { sessionId, ownAgentId });
527
- }
528
- const enqueued = this.eventQueue.enqueue(normalized.event, laneKey);
529
- // ZIG-832 P4: advance this chat's inbox watermark for a message handled on
530
- // the LIVE push path. Without it, only catch-up acks — so on a re-host the
531
- // server watermark still sits before this message and catch-up re-delivers
532
- // it, re-running a finished pipeline (the cross-wake token burn). Mirrors
533
- // catch-up's ack-on-delivery; newer messages carry a later ts and still
534
- // deliver, so a genuinely new turn in the same chat is not lost. Best-effort:
535
- // never block or fail the wake.
536
- const msgTs = (metadata.createdAt ?? metadata.timestamp ?? metadata.sentTimestamp) as string | undefined;
537
- if (normalized.chatId && typeof msgTs === 'string' && msgTs) {
538
- void this._ackChatWatermark(normalized.chatId, msgTs);
539
- } else if (normalized.chatId) {
540
- // ZIG-860: this ack silently not firing is exactly how the re-delivery
541
- // bug hid — a live message with no usable timestamp must be loud.
542
- runtimeLog.warn('AgentHost', `live-ack skipped: no timestamp on message chatId=${normalized.chatId} messageId=${messageId ?? 'null'}`);
543
- }
544
- return enqueued;
545
- }
546
-
547
- /** Best-effort: advance the inbox watermark for a chat handled live (ZIG-832 P4). */
548
- private async _ackChatWatermark(chatId: string, upTo: string): Promise<void> {
549
- try {
550
- if (!this._inbox) {
551
- this._inbox = new InboxClient(this.options.operatorKey!, this.options.agentId!);
552
- }
553
- await this._inbox.ack([{ kind: 'chat', id: chatId, upTo }]);
554
- } catch (e: unknown) {
555
- // warn, not debug (ZIG-860): a failing live-ack means catch-up will
556
- // re-deliver this chat's backlog after the next host eviction.
557
- runtimeLog.warn('AgentHost', `live-ack chat ${chatId} failed: ${(e as Error)?.message ?? e}`);
558
- }
559
- }
560
-
561
- private async _processEvent(event: unknown, laneKey: string): Promise<void> {
562
- const session = this.sessions.get(laneKey);
563
- if (!session?.sessionId) return;
564
-
565
- // Stale timer frames (superseded epoch) are dropped BEFORE touching the
566
- // live timer — a pure-stale frame must not reset a freshly armed countdown.
567
- const frame = this.stateTimers.stripStale(event, laneKey);
568
- if (frame === null) return;
569
-
570
- // Any event reaching the machine invalidates the countdown ("clear on any
571
- // event for the lane"); re-armed below if the tick settles parked+timed.
572
- this.stateTimers.clear(laneKey);
573
-
574
- // Cold-start recovery: restore FSM state from sessionStore when the
575
- // in-memory lane has no prior context (e.g. after a container restart).
576
- if (this.sessionStore && session.ctx === undefined && session.state === undefined) {
577
- try {
578
- const persisted = await this.sessionStore.load(laneKey);
579
- if (persisted) {
580
- session.ctx = persisted.ctx;
581
- session.state = persisted.state;
582
- }
583
- } catch (err: unknown) {
584
- runtimeLog.warn('AgentHost', `sessionStore.load failed laneKey=${laneKey}: ${(err as Error)?.message || err}`);
585
- }
586
- }
587
-
588
- const identity: Identity = {
589
- agentId: session.ownAgentId || this.options.agentId,
590
- sessionId: session.sessionId,
591
- laneKey,
592
- };
593
-
594
- // The watchdog timer must be cleared once the tick settles — leaving it
595
- // armed pinned one live 2-minute timer per tick (and kept short-lived
596
- // processes/tests alive until it fired).
597
- let tickWatchdog: ReturnType<typeof setTimeout> | undefined;
598
- try {
599
- const result = await Promise.race([
600
- this.agent.tick({ event: frame, ctx: session.ctx, state: session.state, identity, eff: this.eff, memory: this.memory, transitionTap: this.options.transitionTap }),
601
- new Promise<never>((_, reject) => {
602
- tickWatchdog = setTimeout(() => reject(new Error('tick timeout')), this.tickTimeoutMs);
603
- }),
604
- ]);
605
- session.ctx = result.ctx;
606
- session.state = result.state;
607
- this.sessions.set(laneKey, session);
608
-
609
- // One line per wake that actually spent LLM budget — the handle for
610
- // tracking a hosted agent live (grep `agent=<id>`). Parked no-op ticks
611
- // (FSM exits, wake-rate-skipped Claude wakes) report zero tokens and
612
- // stay quiet. A regressed self-wake loop shows here as a rapid stream of
613
- // these lines for one lane. Tokens are billable input (incl. cache).
614
- const m = result.metrics;
615
- if (m && (m.tokens?.total ?? 0) > 0) {
616
- runtimeLog.info(
617
- 'AgentHost',
618
- `wake agent=${identity.agentId} lane=${laneKey} llmCalls=${m.llmCalls} ` +
619
- `tokens_in=${m.tokens.input} tokens_out=${m.tokens.output} total=${m.tokens.total}`,
620
- );
621
- }
622
-
623
- // Single source of truth for usage/cost (ZIG-813): emit the tick's
624
- // authoritative token metrics once, for every cognition. Never let an
625
- // observability failure break the tick.
626
- if (this.options.metricsTap && result.metrics) {
627
- try {
628
- this.options.metricsTap({
629
- agentId: identity.agentId,
630
- sessionId: session.sessionId,
631
- model: this.options.model ?? 'unknown',
632
- cognition: this.options.cognition ?? 'fsm',
633
- metrics: result.metrics,
634
- });
635
- } catch (err: unknown) {
636
- runtimeLog.warn('AgentHost', `metricsTap failed: ${(err as Error)?.message || err}`);
637
- }
638
- }
639
-
640
- if (this.sessionStore) {
641
- this.sessionStore.save(laneKey, { ctx: session.ctx, state: session.state }).catch((err: unknown) => {
642
- runtimeLog.warn('AgentHost', `sessionStore.save failed laneKey=${laneKey}: ${(err as Error)?.message || err}`);
643
- });
644
- }
645
-
646
- // Arm only when the timeout was handled: an unhandled state-timeout must
647
- // not re-arm, or the lane would runtimeLog.error every timeoutMs forever.
648
- if (!result.stateTimeoutUnhandled) this._armIfTimedParked(laneKey, result.state);
649
- } catch (err: unknown) {
650
- runtimeLog.warn('AgentHost', `tick failed laneKey=${laneKey}: ${(err as Error)?.message || err}`);
651
- // Deliberate re-arm on failure. Exact scenario: the tick timed out at
652
- // tickTimeoutMs (or sessionStore.load threw) leaving session.state at a
653
- // timed parked state — without re-arming, that lane would never wake
654
- // again unless a real event happens to arrive.
655
- this._armIfTimedParked(laneKey, session.state ?? this.agent.workflow.initial);
656
- // Best-effort: also notify the user so the conversation isn't silently
657
- // stuck (ZIG-607). Complements the timer re-arm above — that recovers a
658
- // parked lane; this tells a waiting human.
659
- const ev = event as Record<string, unknown> | undefined;
660
- const senderType = String(ev?.senderType ?? '').toUpperCase();
661
- const senderId = ev?.senderId as string | undefined;
662
- if (senderId && senderType === 'USER') {
663
- this.eff({ kind: 'send-message', sessionId: session.sessionId, receiverId: senderId, text: 'Something went wrong. Please try again.' }).catch(() => {});
664
- }
665
- } finally {
666
- clearTimeout(tickWatchdog);
667
- }
668
- }
669
-
670
- /** Arms the lane's liveness timer iff stateName is a parked state declaring timeoutMs. */
671
- private _armIfTimedParked(laneKey: string, stateName: string | undefined): void {
672
- if (!stateName) return;
673
- const sd = this.agent.workflow.states[stateName];
674
- if (sd?.kind !== 'parked' || sd.timeoutMs === undefined) return;
675
- this.stateTimers.arm(laneKey, stateName, sd.timeoutMs);
676
- }
677
-
678
- private _resolveOwnAgentId(metadata: Record<string, unknown> = {}): string | null {
679
- const self = metadata.self as Record<string, unknown> | undefined;
680
- const agent = metadata.agent as Record<string, unknown> | undefined;
681
- return this.options.agentId || (self?.id as string) || (agent?.id as string) || (metadata.ziggsAgentId as string) || null;
682
- }
683
-
684
- async connectAsync(timeout?: number) {
685
- await this.wsClient.connectAsync(timeout);
686
- if (this.inboxCatchUpEnabled) {
687
- await this._ensureCatchUp();
688
- // Backend flushPendingMessages can emit after auth; re-poll once so inbox
689
- // ack catches stragglers and we don't leave newMessages > 0 (ZIG-462).
690
- await new Promise((r) => setTimeout(r, 750));
691
- this.catchUpInFlight = null;
692
- await this._ensureCatchUp();
693
- await this._bootstrapAssignedActiveTasks();
694
- }
695
- return this;
696
- }
697
- private async _bootstrapAssignedActiveTasks(): Promise<void> {
698
- const agentId = this.options.agentId;
699
- if (!agentId || !this.taskService) return;
700
-
701
- let tasks: Array<{ taskId?: string; assigneeId?: string | null }> = [];
702
- try {
703
- tasks = await this.taskService.getActiveTasksForAgent(agentId);
704
- } catch (err: unknown) {
705
- runtimeLog.warn(
706
- this.options.name || 'agent',
707
- `active-task bootstrap skipped: ${(err as Error)?.message ?? err}`,
708
- );
709
- return;
710
- }
711
-
712
- for (const row of tasks) {
713
- const taskId = row.taskId;
714
- if (!taskId) continue;
715
- if (row.assigneeId && row.assigneeId !== agentId) continue;
716
-
717
- let full: Record<string, unknown>;
718
- try {
719
- full = (await this.taskService.getTask(taskId)) as unknown as Record<string, unknown>;
720
- } catch {
721
- continue;
722
- }
723
- const state = String(full.state ?? '');
724
- if (state !== 'active' && state !== 'in-progress') continue;
725
-
726
- const sessionId =
727
- (full.agreementId as string | undefined) ||
728
- (full.originChatId as string | undefined) ||
729
- 'default';
730
- const laneKey = `${agentId}:${sessionId}`;
731
- if (!this.sessions.has(laneKey)) {
732
- this.sessions.set(laneKey, { sessionId, ownAgentId: agentId });
733
- }
734
-
735
- runtimeLog.info(
736
- this.options.name || 'agent',
737
- `bootstrap task-assigned ${taskId} (state=${state})`,
738
- );
739
- await this.eventQueue.enqueue(
740
- {
741
- type: 'task_result',
742
- result: {
743
- ...full,
744
- taskId: (full.taskId as string | undefined) ?? taskId,
745
- state,
746
- providerIsYou: true,
747
- },
748
- chatId: full.originChatId as string | undefined,
749
- },
750
- laneKey,
751
- );
752
- }
753
- }
754
-
755
- /** ZIG-802: replay task-assigned for an existing active task (demo recovery). */
756
- async wakeTaskAssignment(taskId: string): Promise<void> {
757
- const agentId = this.options.agentId;
758
- if (!agentId || !this.taskService) {
759
- throw new Error('wakeTaskAssignment: agent not configured');
760
- }
761
- const full = (await this.taskService.getTask(taskId)) as unknown as Record<string, unknown>;
762
- const state = String(full.state ?? '');
763
- if (state !== 'active' && state !== 'in-progress') {
764
- throw new Error(`wakeTaskAssignment: task ${taskId} is ${state}, not active`);
765
- }
766
- const sessionId =
767
- (full.agreementId as string | undefined) ||
768
- (full.originChatId as string | undefined) ||
769
- 'default';
770
- const laneKey = `${agentId}:${sessionId}`;
771
- if (!this.sessions.has(laneKey)) {
772
- this.sessions.set(laneKey, { sessionId, ownAgentId: agentId });
773
- }
774
- await this.eventQueue.enqueue(
775
- {
776
- type: 'task_result',
777
- result: {
778
- ...full,
779
- taskId: (full.taskId as string | undefined) ?? taskId,
780
- state,
781
- providerIsYou: true,
782
- },
783
- chatId: full.originChatId as string | undefined,
784
- },
785
- laneKey,
786
- );
787
- }
788
-
789
- disconnect() { this.stateTimers.clearAll(); this.wsClient.disconnect(); }
790
- /** Waits for all in-flight ticks to complete. Call before process.exit() on SIGTERM. */
791
- drain(timeoutMs = 30_000): Promise<void> { return this.eventQueue.waitForIdle(timeoutMs); }
792
- registerTool(tool: ToolDefinition) { return this.toolManager.register(tool); }
793
- isConnected() { return this.wsClient.isConnected(); }
794
- }
795
-
796
- /**
797
- * createAgent(config) → AgentHost
798
- * createAgentPool([cfg, ...]) → ConnectionPool (lazy fleet, async-loaded)
799
- */
800
- export function createAgent(options: AgentHostOptions | AgentConfig = {}): AgentHost {
801
- return new AgentHost(options as AgentHostOptions);
802
- }
803
-
804
- export async function createAgentPool(
805
- configs: AgentHostOptions[] | AgentConfig[],
806
- opts: { poolOptions?: { maxActive?: number; idleTimeoutMs?: number } } = {},
807
- ): Promise<unknown> {
808
- const ConnectionPool = await loadConnectionPool();
809
- const pool = new ConnectionPool(opts.poolOptions);
810
- (pool as { register: (configs: unknown[]) => void }).register(configs);
811
- return pool;
812
- }
813
-
814
- export default AgentHost;