@ziggs-ai/agent-sdk 0.1.3 → 0.1.4

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 (85) hide show
  1. package/README.md +1 -1
  2. package/package.json +9 -4
  3. package/src/AgentHost.ts +342 -0
  4. package/src/adapters/OpenAIAdapter.ts +125 -0
  5. package/src/agent/Agent.ts +98 -0
  6. package/src/cognition/validateContext.ts +95 -0
  7. package/src/context/applyEffects.ts +80 -0
  8. package/src/context/batch.ts +17 -0
  9. package/src/context/classifyEnvelope.ts +38 -0
  10. package/src/context/routingLabels.ts +46 -0
  11. package/src/defineAgent.ts +62 -0
  12. package/src/formatters/AgreementFormatter.ts +111 -0
  13. package/src/formatters/HistoryFormatter.ts +166 -0
  14. package/src/formatters/index.ts +2 -0
  15. package/src/index.ts +86 -0
  16. package/src/ingress/normalizeIncoming.ts +119 -0
  17. package/src/memory/MemoryStore.ts +104 -0
  18. package/src/runtime/AgentMachine.ts +298 -0
  19. package/src/runtime/PromptBuilder.ts +461 -0
  20. package/src/runtime/buildOutcome.ts +488 -0
  21. package/src/runtime/defaults.ts +72 -0
  22. package/src/runtime/runTurn.ts +637 -0
  23. package/src/runtime/validateWorkflow.ts +165 -0
  24. package/src/server/ConnectionPool.ts +155 -0
  25. package/src/server/EventQueue.ts +119 -0
  26. package/src/server/OutboxBuffer.ts +90 -0
  27. package/src/server/ZiggsEffectHandler.ts +335 -0
  28. package/src/server/agreements/AgreementService.ts +111 -0
  29. package/src/server/createHealthServer.ts +8 -0
  30. package/src/server/proactive/ProactiveTrigger.ts +83 -0
  31. package/src/server/runLauncher.ts +131 -0
  32. package/src/server/tasks/TaskService.ts +111 -0
  33. package/src/server/tasks/index.ts +4 -0
  34. package/src/server/tasks/paymentTools.ts +156 -0
  35. package/src/server/tasks/protocolRunner.ts +101 -0
  36. package/src/server/tasks/protocolTools.ts +96 -0
  37. package/src/server/ziggspay/ZiggsPayClient.ts +193 -0
  38. package/src/shared/ids.ts +3 -0
  39. package/src/shared/runtimeLog.ts +72 -0
  40. package/src/shared/types.ts +31 -0
  41. package/src/tasks/protocolRegistry.ts +25 -0
  42. package/src/tasks/taskCore.ts +139 -0
  43. package/src/tools/ToolManager.ts +95 -0
  44. package/src/tools/{ToolProvider.js → ToolProvider.ts} +5 -15
  45. package/src/tools/defineTool.ts +90 -0
  46. package/src/tools/index.ts +5 -0
  47. package/src/types.ts +368 -0
  48. package/src/utils/jsonExtractor.ts +100 -0
  49. package/src/ConnectionPool.js +0 -133
  50. package/src/adapters/OpenAIAdapter.js +0 -73
  51. package/src/agent/Agent.js +0 -121
  52. package/src/agent/EventQueue.js +0 -68
  53. package/src/agent/OutboxBuffer.js +0 -62
  54. package/src/cognition/PromptBuilder.js +0 -312
  55. package/src/cognition/resolveActionTool.js +0 -12
  56. package/src/cognition/runTurn.js +0 -578
  57. package/src/context/applyEffects.js +0 -133
  58. package/src/context/batch.js +0 -25
  59. package/src/context/classifyEnvelope.js +0 -82
  60. package/src/context/routingLabels.js +0 -54
  61. package/src/createHealthServer.js +0 -28
  62. package/src/formatters/HistoryFormatter.js +0 -257
  63. package/src/formatters/TaskFormatter.js +0 -180
  64. package/src/formatters/index.js +0 -9
  65. package/src/index.js +0 -76
  66. package/src/ingress/normalizeIncoming.js +0 -70
  67. package/src/runLauncher.js +0 -159
  68. package/src/shared/ids.js +0 -7
  69. package/src/shared/types.js +0 -86
  70. package/src/tasks/TaskService.js +0 -247
  71. package/src/tasks/index.js +0 -9
  72. package/src/tasks/taskCore.js +0 -229
  73. package/src/tasks/taskProtocolRegistry.js +0 -22
  74. package/src/tasks/taskProtocolRunner.js +0 -107
  75. package/src/tasks/taskProtocolTools.js +0 -87
  76. package/src/tools/ToolManager.js +0 -79
  77. package/src/tools/defineTool.js +0 -82
  78. package/src/tools/index.js +0 -11
  79. package/src/utils/jsonExtractor.js +0 -139
  80. package/src/workflow/AgentMachine.js +0 -250
  81. package/src/workflow/WorkflowRuntime.js +0 -63
  82. package/src/workflow/dsl.js +0 -287
  83. package/src/workflow/motifs.js +0 -435
  84. package/src/ziggs/runtime.js +0 -192
  85. /package/src/adapters/{index.js → index.ts} +0 -0
package/README.md CHANGED
@@ -62,7 +62,7 @@ Examples in the repo: `examples/agents/*.js` (e.g. coffee, expense, delivery age
62
62
  - **`ZiggsAgent`**, **`createAgent`**, **`defineAgent`**
63
63
  - **`Agent`**, **`AgentMachine`**, **`runTurn`**
64
64
  - **Prompt / tools:** `PromptBuilder`, `ToolManager`, `defineTool`
65
- - **Task tools:** `taskMakeTaskTool`, `taskUpdateTaskTool`, `taskRespondProposalTool`, `taskMakeSubTasksTool`, `taskUpdatePlanStepTool`, `TASK_PROTOCOL_TOOLS`
65
+ - **Protocol tools:** `agreementProposeTool`, `agreementDelegateTool`, `agreementRespondTool`, `agreementCounterProposalTool`, `taskSpawnTool`, `taskUpdateTool`, `taskUpdatePlanStepTool`, `PROTOCOL_TOOLS`
66
66
  - **Adapters / utils:** `OpenAIAdapter`, JSON helpers, formatters
67
67
  - **Re-exported** from `@ziggs-ai/api-client`: `WebSocketClient`, `ContextReader`, `ContextWriter`, URL helpers, etc.
68
68
 
package/package.json CHANGED
@@ -1,17 +1,22 @@
1
1
  {
2
2
  "name": "@ziggs-ai/agent-sdk",
3
- "version": "0.1.3",
3
+ "version": "0.1.4",
4
4
  "description": "Agent framework SDK for building autonomous agents on the Ziggs platform",
5
5
  "type": "module",
6
- "main": "src/index.js",
6
+ "main": "src/index.ts",
7
7
  "exports": {
8
- ".": "./src/index.js"
8
+ ".": "./src/index.ts"
9
9
  },
10
10
  "engines": {
11
11
  "node": ">=18"
12
12
  },
13
+ "scripts": {
14
+ "typecheck": "tsc -p tsconfig.json --noEmit",
15
+ "test": "node --experimental-test-module-mocks --import tsx/esm --test --test-reporter=spec test/*.test.ts",
16
+ "test:watch": "node --experimental-test-module-mocks --import tsx/esm --test --watch test/*.test.ts"
17
+ },
13
18
  "dependencies": {
14
- "@ziggs-ai/api-client": "^0.1.3",
19
+ "@ziggs-ai/api-client": "^0.1.4",
15
20
  "ajv": "^8.17.1",
16
21
  "dotenv": "^17.2.3",
17
22
  "openai": "^4.0.0"
@@ -0,0 +1,342 @@
1
+ import type { AgentConfig, Ctx, EffectHandler, Identity, Workflow } from './types.js';
2
+ import { Agent } from './agent/Agent.js';
3
+ import { EventQueue } from './server/EventQueue.js';
4
+ import { createZiggsEffectHandler } from './server/ZiggsEffectHandler.js';
5
+ import { normalizeIncomingEvent, type NormalizedEvent } from './ingress/normalizeIncoming.js';
6
+ import { PromptBuilder } from './runtime/PromptBuilder.js';
7
+ import { InMemoryStore, type MemoryStore } from './memory/MemoryStore.js';
8
+ import { ToolManager } from './tools/ToolManager.js';
9
+ import type { ToolDefinition } from './tools/defineTool.js';
10
+ import { PROTOCOL_TOOLS } from './server/tasks/protocolTools.js';
11
+ import { OpenAIAdapter } from './adapters/OpenAIAdapter.js';
12
+ import {
13
+ WebSocketClient,
14
+ MessagesClient,
15
+ ArtifactsClient,
16
+ ScopeClient,
17
+ TelemetryClient,
18
+ type MessageMetadata,
19
+ } from '@ziggs-ai/api-client';
20
+ import { TaskService } from './server/tasks/TaskService.js';
21
+ import { AgreementService } from './server/agreements/AgreementService.js';
22
+ import { runtimeLog } from './shared/runtimeLog.js';
23
+
24
+ // ConnectionPool lives in a separate module; resolved via a computed-path
25
+ // dynamic import to avoid a circular import with AgentHost.
26
+ async function loadConnectionPool(): Promise<new (opts?: { maxActive?: number; idleTimeoutMs?: number }) => unknown> {
27
+ const modPath = './server/ConnectionPool.js';
28
+ const mod = await import(/* @vite-ignore */ modPath) as { ConnectionPool: new (opts?: { maxActive?: number; idleTimeoutMs?: number }) => unknown };
29
+ return mod.ConnectionPool;
30
+ }
31
+
32
+ /**
33
+ * Given a normalized event, return the sessionId for this agent. Return
34
+ * `null` to skip processing. Lets each Agent define what "session" means —
35
+ * chat-scoped agents bucket by chatId (the default), task-scoped agents
36
+ * could bucket by taskId, cron agents by schedule id, etc.
37
+ */
38
+ export type SessionResolver = (normalized: NormalizedEvent, ownAgentId: string) => string | null;
39
+
40
+ const DEFAULT_SESSION_RESOLVER: SessionResolver = (normalized) => normalized.chatId;
41
+
42
+ interface AgentServices {
43
+ tools?: ToolDefinition[];
44
+ llm?: { model?: string };
45
+ [key: string]: unknown;
46
+ }
47
+
48
+ export interface AgentHostOptions {
49
+ openaiKey?: string;
50
+ model?: string;
51
+ operatorKey?: string;
52
+ agentId?: string;
53
+ name?: string | null;
54
+ wsUrl?: string;
55
+ description?: string;
56
+ specialization?: string | null;
57
+ tools?: ToolDefinition[];
58
+ taskTools?: 'all' | 'none' | string[];
59
+ services?: AgentServices;
60
+ workflow?: Workflow;
61
+ // Convenience: states + initial without an explicit workflow.
62
+ states?: Workflow['states'];
63
+ initial?: string;
64
+ tickTimeoutMs?: number;
65
+ /** Per-agent session keying. Defaults to `(n) => n.chatId`. */
66
+ sessionResolver?: SessionResolver;
67
+ /**
68
+ * Agent-private long-term memory. Operator-provided. Default: a fresh
69
+ * `InMemoryStore` (non-durable; lost on process restart). For durability,
70
+ * pass `new FileMemoryStore(path)` or your own implementation.
71
+ */
72
+ memory?: MemoryStore;
73
+ }
74
+
75
+ function createToolManager(options: AgentHostOptions): ToolManager {
76
+ const toolManager = new ToolManager();
77
+ const userTools = options.tools || options.services?.tools || [];
78
+ let taskTools: ToolDefinition[];
79
+ if (options.taskTools === 'none') taskTools = [];
80
+ else if (Array.isArray(options.taskTools)) taskTools = PROTOCOL_TOOLS.filter((t: ToolDefinition) => (options.taskTools as string[]).includes(t.schema.function.name as string));
81
+ else taskTools = PROTOCOL_TOOLS; // 'all' or undefined
82
+ toolManager.registerAll([...taskTools, ...(Array.isArray(userTools) ? userTools : [])]);
83
+ return toolManager;
84
+ }
85
+
86
+ function createBackendServices(options: AgentHostOptions) {
87
+ const toolManager = createToolManager(options);
88
+ const llm = new OpenAIAdapter({ key: options.openaiKey, model: options.services?.llm?.model || options.model });
89
+ const operatorKey = options.operatorKey!;
90
+ const agentId = options.agentId!;
91
+ const messagesClient = new MessagesClient(operatorKey, agentId);
92
+ const artifactsClient = new ArtifactsClient(operatorKey, agentId);
93
+ const scopeClient = new ScopeClient(operatorKey, agentId);
94
+ const promptBuilder = new PromptBuilder({ description: options.description, specialization: options.specialization });
95
+ const taskService = new TaskService(operatorKey, agentId);
96
+ const agreementService = new AgreementService(operatorKey, agentId);
97
+ const telemetryClient = new TelemetryClient(operatorKey, agentId);
98
+ return {
99
+ toolManager,
100
+ llm,
101
+ messagesClient,
102
+ artifactsClient,
103
+ scopeClient,
104
+ promptBuilder,
105
+ taskService,
106
+ agreementService,
107
+ telemetryClient,
108
+ };
109
+ }
110
+
111
+ interface LaneSession {
112
+ sessionId: string;
113
+ ownAgentId: string;
114
+ ctx?: Ctx;
115
+ state?: string;
116
+ }
117
+
118
+ /**
119
+ * AgentHost — the Server. Owns a pure {@link Agent}, an {@link EffectHandler},
120
+ * a transport (Ziggs WebSocket), and the lane router (per-session FIFO queue
121
+ * + in-memory session state).
122
+ *
123
+ * transport (WS) → handleMessage → agent.tick(... , eff) → ...
124
+ */
125
+ export class AgentHost {
126
+ options: Required<Pick<AgentHostOptions, 'agentId' | 'operatorKey' | 'openaiKey'>> & AgentHostOptions;
127
+ toolManager!: ToolManager;
128
+ llm!: OpenAIAdapter; // retained for external inspection
129
+ agent!: Agent;
130
+ wsClient!: WebSocketClient;
131
+ private eff!: EffectHandler;
132
+ private eventQueue!: EventQueue;
133
+ private sessions = new Map<string, LaneSession>();
134
+ private sessionResolver: SessionResolver;
135
+ private tickTimeoutMs: number;
136
+ private memory: MemoryStore;
137
+
138
+ constructor(options: AgentHostOptions = {}) {
139
+ this.options = {
140
+ openaiKey: options.openaiKey || process.env.OPENAI_API_KEY,
141
+ model: options.model || options.services?.llm?.model || 'gpt-5.4',
142
+ operatorKey: options.operatorKey || process.env.ZIGGS_OPERATOR_KEY || undefined,
143
+ agentId: options.agentId || '',
144
+ name: options.name || null,
145
+ wsUrl: options.wsUrl,
146
+ description: options.description || 'A helpful assistant',
147
+ specialization: options.specialization ?? null,
148
+ tools: options.tools || [],
149
+ taskTools: options.taskTools,
150
+ states: options.states,
151
+ initial: options.initial,
152
+ services: options.services,
153
+ workflow: options.workflow,
154
+ tickTimeoutMs: options.tickTimeoutMs,
155
+ sessionResolver: options.sessionResolver,
156
+ } as Required<Pick<AgentHostOptions, 'agentId' | 'operatorKey' | 'openaiKey'>> & AgentHostOptions;
157
+ this.sessionResolver = options.sessionResolver || DEFAULT_SESSION_RESOLVER;
158
+ this.tickTimeoutMs = options.tickTimeoutMs ?? 120_000;
159
+ this.memory = options.memory ?? new InMemoryStore();
160
+ this._validate();
161
+ this._setup();
162
+ }
163
+
164
+ private _validate(): void {
165
+ if (!this.options.openaiKey) throw new Error('openaiKey is required');
166
+ if (!this.options.operatorKey) throw new Error('operatorKey is required — pass { operatorKey } or set ZIGGS_OPERATOR_KEY');
167
+ if (!this.options.agentId) throw new Error('agentId is required');
168
+ }
169
+
170
+ private _setup(): void {
171
+ const services = createBackendServices(this.options);
172
+ this.toolManager = services.toolManager;
173
+ this.llm = services.llm;
174
+
175
+ this.wsClient = new WebSocketClient({
176
+ wsUrl: this.options.wsUrl,
177
+ operatorKey: this.options.operatorKey,
178
+ agentId: this.options.agentId,
179
+ label: this.options.name || 'agent',
180
+ });
181
+
182
+ const messageSender = (message: string, receiverId: string, sessionId: string): Promise<unknown> => {
183
+ this.wsClient.send(sessionId, receiverId, message);
184
+ return Promise.resolve(undefined);
185
+ };
186
+
187
+ let workflow = this.options.workflow;
188
+ if (!workflow && this.options.states) {
189
+ workflow = {
190
+ id: this.options.agentId,
191
+ description: this.options.description!,
192
+ initial: this.options.initial || 'idle',
193
+ states: this.options.states,
194
+ };
195
+ }
196
+ if (!workflow?.states) throw new Error('AgentHost: workflow (or states + initial) is required');
197
+
198
+ this.eff = createZiggsEffectHandler({
199
+ llm: services.llm as unknown as import('./server/ZiggsEffectHandler.js').ZiggsEffectDeps['llm'],
200
+ messagesClient: services.messagesClient,
201
+ artifactsClient: services.artifactsClient,
202
+ scopeClient: services.scopeClient,
203
+ taskService: services.taskService,
204
+ agreementService: services.agreementService,
205
+ messageSender,
206
+ toolManager: services.toolManager,
207
+ operatorKey: this.options.operatorKey!,
208
+ agentId: this.options.agentId!,
209
+ telemetryClient: services.telemetryClient,
210
+ });
211
+
212
+ this.agent = new Agent({
213
+ workflow,
214
+ toolManager: services.toolManager,
215
+ promptBuilder: services.promptBuilder,
216
+ });
217
+
218
+ this.eventQueue = new EventQueue((event, laneKey) => this._processEvent(event, laneKey));
219
+
220
+ this.wsClient.setMessageHandler((text: string, metadata: MessageMetadata) => this.handleMessage(text, metadata as unknown as Record<string, unknown>));
221
+ this.wsClient.setResourceEventHandler((event) => this.handleResourceEvent(event as unknown as Record<string, unknown>));
222
+ }
223
+
224
+ /**
225
+ * Push notification handler — invoked when the backend sees a relevant
226
+ * resource change (artifact created, task state transition, agreement
227
+ * updated). The agent decides which session/lane to wake based on the
228
+ * event payload; it pulls the actual data via the primitive endpoints
229
+ * from inside the tick.
230
+ *
231
+ * Lightweight: defers all work to a tick by enqueueing a synthetic event
232
+ * carrying just enough context for the agent to route it.
233
+ */
234
+ async handleResourceEvent(event: Record<string, unknown>): Promise<void> {
235
+ if (!event || typeof event !== 'object') return;
236
+ const ownAgentId = this.options.agentId;
237
+ if (!ownAgentId) return;
238
+
239
+ // Map the event to a candidate session key using the configured resolver
240
+ // on a synthetic normalized-event shape. Default resolver buckets by
241
+ // chatId; agents using a different sessionResolver (agreementId, taskId,
242
+ // counterpartyId) will get the right lane here as long as the wire
243
+ // event carries the corresponding id.
244
+ const fakeNormalized = {
245
+ chatId: event.chatId as string | null,
246
+ event: { type: 'message' as const, timestamp: Date.now(), text: '' },
247
+ shouldProcess: true,
248
+ reason: null,
249
+ } as NormalizedEvent;
250
+ const sessionId = this.sessionResolver(fakeNormalized, ownAgentId);
251
+ if (!sessionId) return;
252
+
253
+ const laneKey = `${ownAgentId}:${sessionId}`;
254
+ if (!this.sessions.has(laneKey)) {
255
+ this.sessions.set(laneKey, { sessionId, ownAgentId });
256
+ }
257
+ return this.eventQueue.enqueue({ type: 'resource_changed', resource: event, chatId: event.chatId, taskId: event.taskId, agreementId: event.agreementId }, laneKey);
258
+ }
259
+
260
+ /** Wire entry. Normalize → resolve session → enqueue on the right lane. */
261
+ async handleMessage(text: string, metadata: Record<string, unknown> = {}): Promise<void> {
262
+ const ownAgentId = this._resolveOwnAgentId(metadata);
263
+ if (!ownAgentId) {
264
+ runtimeLog.warn('AgentHost', 'handleMessage: ownAgentId missing — dropping event');
265
+ return;
266
+ }
267
+
268
+ const normalized = normalizeIncomingEvent({ text, metadata, ownAgentId });
269
+ if (!normalized.shouldProcess) {
270
+ runtimeLog.info('AgentHost', `skip reason=${normalized.reason || 'not_relevant'}`);
271
+ return;
272
+ }
273
+
274
+ const sessionId = this.sessionResolver(normalized, ownAgentId);
275
+ if (!sessionId) {
276
+ runtimeLog.warn('AgentHost', 'session resolver returned null, skipping');
277
+ return;
278
+ }
279
+
280
+ const laneKey = `${ownAgentId}:${sessionId}`;
281
+ if (!this.sessions.has(laneKey)) {
282
+ this.sessions.set(laneKey, { sessionId, ownAgentId });
283
+ }
284
+ return this.eventQueue.enqueue(normalized.event, laneKey);
285
+ }
286
+
287
+ private async _processEvent(event: unknown, laneKey: string): Promise<void> {
288
+ const session = this.sessions.get(laneKey);
289
+ if (!session?.sessionId) return;
290
+
291
+ const identity: Identity = {
292
+ agentId: session.ownAgentId || this.options.agentId,
293
+ sessionId: session.sessionId,
294
+ laneKey,
295
+ };
296
+
297
+ try {
298
+ const result = await Promise.race([
299
+ this.agent.tick({ event, ctx: session.ctx, state: session.state, identity, eff: this.eff, memory: this.memory }),
300
+ new Promise<never>((_, reject) =>
301
+ setTimeout(() => reject(new Error('tick timeout')), this.tickTimeoutMs),
302
+ ),
303
+ ]);
304
+ session.ctx = result.ctx;
305
+ session.state = result.state;
306
+ this.sessions.set(laneKey, session);
307
+ } catch (err: unknown) {
308
+ runtimeLog.warn('AgentHost', `tick failed laneKey=${laneKey}: ${(err as Error)?.message || err}`);
309
+ }
310
+ }
311
+
312
+ private _resolveOwnAgentId(metadata: Record<string, unknown> = {}): string | null {
313
+ const self = metadata.self as Record<string, unknown> | undefined;
314
+ const agent = metadata.agent as Record<string, unknown> | undefined;
315
+ return this.options.agentId || (self?.id as string) || (agent?.id as string) || (metadata.ziggsAgentId as string) || null;
316
+ }
317
+
318
+ connectAsync(timeout?: number) { return this.wsClient.connectAsync(timeout); }
319
+ disconnect() { this.wsClient.disconnect(); }
320
+ registerTool(tool: ToolDefinition) { return this.toolManager.register(tool); }
321
+ isConnected() { return this.wsClient.isConnected(); }
322
+ }
323
+
324
+ /**
325
+ * createAgent(config) → AgentHost
326
+ * createAgentPool([cfg, ...]) → ConnectionPool (lazy fleet, async-loaded)
327
+ */
328
+ export function createAgent(options: AgentHostOptions | AgentConfig = {}): AgentHost {
329
+ return new AgentHost(options as AgentHostOptions);
330
+ }
331
+
332
+ export async function createAgentPool(
333
+ configs: AgentHostOptions[] | AgentConfig[],
334
+ opts: { poolOptions?: { maxActive?: number; idleTimeoutMs?: number } } = {},
335
+ ): Promise<unknown> {
336
+ const ConnectionPool = await loadConnectionPool();
337
+ const pool = new ConnectionPool(opts.poolOptions);
338
+ (pool as { register: (configs: unknown[]) => void }).register(configs);
339
+ return pool;
340
+ }
341
+
342
+ export default AgentHost;
@@ -0,0 +1,125 @@
1
+ import OpenAI from 'openai';
2
+ import type { ToolDefinition } from '../tools/defineTool.js';
3
+ import { runtimeLog } from '../shared/runtimeLog.js';
4
+
5
+ interface ChatResult {
6
+ content: string | null;
7
+ tool_calls: OpenAI.Chat.ChatCompletionMessageToolCall[] | null;
8
+ message: OpenAI.Chat.ChatCompletionMessage;
9
+ usage: OpenAI.CompletionUsage | undefined;
10
+ }
11
+
12
+ interface AdapterOptions {
13
+ key?: string;
14
+ model?: string;
15
+ }
16
+
17
+ export class OpenAIAdapter {
18
+ private client: OpenAI;
19
+ model: string;
20
+
21
+ constructor({ key, model = 'gpt-5.4' }: AdapterOptions) {
22
+ this.client = new OpenAI({ apiKey: key });
23
+ this.model = model;
24
+ }
25
+
26
+ async chat(
27
+ prompt: string,
28
+ tools: ToolDefinition[] = [],
29
+ temperature = 0.2,
30
+ systemHints: OpenAI.Chat.ChatCompletionMessageParam[] = [],
31
+ options: Record<string, unknown> = {},
32
+ ): Promise<ChatResult> {
33
+ const messages: OpenAI.Chat.ChatCompletionMessageParam[] = [
34
+ { role: 'system', content: 'Respond with valid JSON.' },
35
+ ...systemHints,
36
+ { role: 'user', content: prompt },
37
+ ];
38
+ return this.chatMessages(messages, tools, { ...options, temperature });
39
+ }
40
+
41
+ async chatMessages(
42
+ messages: OpenAI.Chat.ChatCompletionMessageParam[],
43
+ tools: ToolDefinition[] = [],
44
+ options: Record<string, unknown> = {},
45
+ ): Promise<ChatResult> {
46
+ const openAITools: OpenAI.Chat.ChatCompletionTool[] | undefined = tools.length > 0
47
+ ? tools.map(t => {
48
+ const fn = (t.schema.function || t.schema) as Record<string, unknown>;
49
+ const tAny = t as unknown as Record<string, unknown>;
50
+ const usageWhen = tAny['usage']
51
+ ? ` Use when: ${(tAny['usage'] as Record<string, unknown>)['when']}`
52
+ : '';
53
+ return {
54
+ type: 'function' as const,
55
+ function: {
56
+ name: fn['name'] as string,
57
+ description: ((fn['description'] as string) || '') + usageWhen,
58
+ parameters: fn['parameters'] as Record<string, unknown>,
59
+ },
60
+ };
61
+ })
62
+ : undefined;
63
+
64
+ try {
65
+ const { usage: _u, ...restOptions } = options as Record<string, unknown>;
66
+ // Verbose LLM request/response logging — commented out to reduce log noise.
67
+ // Re-enable by uncommenting to see full prompt/output blocks per LLM call.
68
+ // const divider = '─'.repeat(80);
69
+ // console.log(`\n${divider}`);
70
+ // console.log(`📤 LLM REQUEST → ${this.model} (${messages.length} msg${messages.length === 1 ? '' : 's'})`);
71
+ // console.log(divider);
72
+ // for (const m of messages) {
73
+ // const content = typeof m.content === 'string' ? m.content : JSON.stringify(m.content);
74
+ // const len = content?.length ?? 0;
75
+ // console.log(`\n[${m.role.toUpperCase()}] ${len} chars`);
76
+ // console.log(content ?? '');
77
+ // }
78
+ // console.log(`${divider}\n`);
79
+ const res = await this.client.chat.completions.create({
80
+ model: this.model,
81
+ messages,
82
+ stream: false,
83
+ max_completion_tokens: 3000,
84
+ response_format: openAITools ? undefined : ((restOptions['response_format'] as OpenAI.ResponseFormatJSONObject) ?? { type: 'json_object' }),
85
+ ...(openAITools && { tools: openAITools }),
86
+ ...restOptions,
87
+ } as OpenAI.Chat.ChatCompletionCreateParamsNonStreaming);
88
+
89
+ const msg = res.choices[0].message;
90
+ const content = msg.content?.trim() ?? null;
91
+ const toolCalls = msg.tool_calls?.length ? msg.tool_calls : null;
92
+ // Verbose LLM response logging — commented out to reduce log noise.
93
+ // console.log(`\n${divider}`);
94
+ // console.log(`📥 LLM RESPONSE ← ${this.model}`);
95
+ // console.log(divider);
96
+ // if (content) {
97
+ // console.log(`\n[CONTENT] ${content.length} chars`);
98
+ // console.log(content);
99
+ // }
100
+ // if (toolCalls) {
101
+ // console.log(`\n[TOOL CALLS] ${toolCalls.length}`);
102
+ // for (const tc of toolCalls) {
103
+ // console.log(` → ${tc.function.name}(${tc.function.arguments})`);
104
+ // }
105
+ // }
106
+ // if (!content && !toolCalls) console.log('\n(no content, no tool calls)');
107
+ // console.log(`${divider}\n`);
108
+ return {
109
+ content,
110
+ tool_calls: toolCalls,
111
+ message: msg,
112
+ usage: res.usage,
113
+ };
114
+ } catch (error) {
115
+ const err = error as Error & { status?: number; code?: string };
116
+ runtimeLog.error('OpenAIAdapter', `LLM API call failed: ${err.message}`, {
117
+ model: this.model,
118
+ error: err.message,
119
+ status: err.status || 'unknown',
120
+ code: err.code || 'unknown',
121
+ });
122
+ throw error;
123
+ }
124
+ }
125
+ }
@@ -0,0 +1,98 @@
1
+ import type { Ctx, Identity, Workflow, EffectHandler } from '../types.js';
2
+ import { AgentMachine } from '../runtime/AgentMachine.js';
3
+ import { runTurn } from '../runtime/runTurn.js';
4
+ import { PromptBuilder } from '../runtime/PromptBuilder.js';
5
+ import type { MemoryStore } from '../memory/MemoryStore.js';
6
+ import type { ToolManager } from '../tools/ToolManager.js';
7
+ import type { RawEvent } from '../runtime/buildOutcome.js';
8
+
9
+ export interface AgentOptions {
10
+ workflow: Workflow;
11
+ toolManager: ToolManager;
12
+ promptBuilder?: PromptBuilder;
13
+ }
14
+
15
+ export interface TickInput {
16
+ /** Wire event (already normalized: a `message` or `task_result`-style envelope). */
17
+ event: unknown;
18
+ /** Server-provided per-session state. Omit on the first tick — a fresh ctx is constructed. */
19
+ ctx?: Ctx;
20
+ /** Server-provided workflow state name. Omit on the first tick — defaults to `workflow.initial`. */
21
+ state?: string;
22
+ /** Who am I and what session am I running in. The Agent carries this through Ctx. */
23
+ identity: Identity;
24
+ /** The single side channel for I/O — LLM, context, tools, send, record. */
25
+ eff: EffectHandler;
26
+ /**
27
+ * Agent-private long-term memory. Operator-provided (default
28
+ * `InMemoryStore` when omitted by `AgentHost`). Key shape is the agent's
29
+ * choice — `counterparty:<id>`, `agreement:<id>:notes`, etc.
30
+ */
31
+ memory?: MemoryStore;
32
+ }
33
+
34
+ export interface TickOutput {
35
+ /** Updated ctx — Server persists this against the session. */
36
+ ctx: Ctx;
37
+ /** Workflow state name after the tick — Server persists this against the session. */
38
+ state: string;
39
+ /** Whether the machine settled at a parked state. */
40
+ parked: boolean;
41
+ /** Accumulated LLM call count and token usage for this tick. */
42
+ metrics: { llmCalls: number; tokens: { total: number; input: number; output: number } };
43
+ }
44
+
45
+ /**
46
+ * Pure agent: workflow definition + cognition machinery. No I/O, no transport,
47
+ * no services. The Agent's only side channel is the `eff` handler it receives
48
+ * on each `tick`. Server-side state (sessions, lanes, queue, outbox) lives in
49
+ * `AgentHost` — not here.
50
+ *
51
+ * The contract:
52
+ *
53
+ * `tick({ event, ctx, state, identity, eff })` →
54
+ * `{ ctx, state, parked }`
55
+ *
56
+ * Reads top-to-bottom like a synchronous function. Every `await eff(...)`
57
+ * inside the cognition core is a yield to the Server, which performs the
58
+ * impure operation and resumes the Agent with the result.
59
+ */
60
+ export class Agent {
61
+ workflow: Workflow;
62
+ toolManager: ToolManager;
63
+ promptBuilder: PromptBuilder;
64
+
65
+ constructor({ workflow, toolManager, promptBuilder }: AgentOptions) {
66
+ if (!workflow?.states) throw new Error('Agent: workflow with states is required');
67
+ if (!toolManager) throw new Error('Agent: toolManager is required');
68
+ this.workflow = workflow;
69
+ this.toolManager = toolManager;
70
+ this.promptBuilder = promptBuilder || new PromptBuilder();
71
+ }
72
+
73
+ async tick({ event, ctx, state, identity, eff, memory }: TickInput): Promise<TickOutput> {
74
+ if (!identity?.sessionId) throw new Error('Agent.tick: identity.sessionId is required');
75
+ if (!eff) throw new Error('Agent.tick: eff is required');
76
+
77
+ const machine = new AgentMachine({
78
+ workflow: this.workflow,
79
+ executionCore: runTurn,
80
+ eff,
81
+ toolManager: this.toolManager,
82
+ promptBuilder: this.promptBuilder,
83
+ memory,
84
+ input: { identity, ctx, state },
85
+ });
86
+
87
+ // `send` awaits the recursive _step until the machine settles at a
88
+ // parked state (or stops). When it returns, we have the final state.
89
+ await machine.send({ event: event as RawEvent | undefined, identity });
90
+
91
+ return {
92
+ ctx: machine.ctx,
93
+ state: machine.state,
94
+ parked: machine.isParked,
95
+ metrics: { llmCalls: machine.llmCalls, tokens: machine.tokens },
96
+ };
97
+ }
98
+ }