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