@ziggs-ai/agent-sdk 0.1.3
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/README.md +82 -0
- package/package.json +26 -0
- package/src/ConnectionPool.js +133 -0
- package/src/adapters/OpenAIAdapter.js +73 -0
- package/src/adapters/index.js +1 -0
- package/src/agent/Agent.js +121 -0
- package/src/agent/EventQueue.js +68 -0
- package/src/agent/OutboxBuffer.js +62 -0
- package/src/cognition/PromptBuilder.js +312 -0
- package/src/cognition/resolveActionTool.js +12 -0
- package/src/cognition/runTurn.js +578 -0
- package/src/context/applyEffects.js +133 -0
- package/src/context/batch.js +25 -0
- package/src/context/classifyEnvelope.js +82 -0
- package/src/context/routingLabels.js +54 -0
- package/src/createHealthServer.js +28 -0
- package/src/formatters/HistoryFormatter.js +257 -0
- package/src/formatters/TaskFormatter.js +180 -0
- package/src/formatters/index.js +9 -0
- package/src/index.js +76 -0
- package/src/ingress/normalizeIncoming.js +70 -0
- package/src/runLauncher.js +159 -0
- package/src/shared/ids.js +7 -0
- package/src/shared/types.js +86 -0
- package/src/tasks/TaskService.js +247 -0
- package/src/tasks/index.js +9 -0
- package/src/tasks/taskCore.js +229 -0
- package/src/tasks/taskProtocolRegistry.js +22 -0
- package/src/tasks/taskProtocolRunner.js +107 -0
- package/src/tasks/taskProtocolTools.js +87 -0
- package/src/tools/ToolManager.js +79 -0
- package/src/tools/ToolProvider.js +29 -0
- package/src/tools/defineTool.js +82 -0
- package/src/tools/index.js +11 -0
- package/src/utils/jsonExtractor.js +139 -0
- package/src/workflow/AgentMachine.js +250 -0
- package/src/workflow/WorkflowRuntime.js +63 -0
- package/src/workflow/dsl.js +287 -0
- package/src/workflow/motifs.js +435 -0
- package/src/ziggs/runtime.js +192 -0
|
@@ -0,0 +1,435 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Motifs — higher-level DSL over the AgentMachine "assembly".
|
|
3
|
+
*
|
|
4
|
+
* Each motif returns a Fragment:
|
|
5
|
+
* { entry: string, states: { [name]: stateDef } }
|
|
6
|
+
*
|
|
7
|
+
* A Fragment's transitions use the sentinel target '__NEXT__' for outbound
|
|
8
|
+
* edges. `seq(...)` rewrites '__NEXT__' to the next fragment's entry; the
|
|
9
|
+
* final fragment rewrites to opts.end (default 'idle').
|
|
10
|
+
*
|
|
11
|
+
* `compile(shape, { initial, end })` returns a states map ready for
|
|
12
|
+
* defineAgent.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
const NEXT = '__NEXT__';
|
|
16
|
+
|
|
17
|
+
// ── Low-level helpers ─────────────────────────────────────────────────────
|
|
18
|
+
|
|
19
|
+
const prefixState = (prefix, name) => (prefix ? `${prefix}_${name}` : name);
|
|
20
|
+
|
|
21
|
+
function mapTargets(stateDef, fn) {
|
|
22
|
+
if (!stateDef.transitions) return stateDef;
|
|
23
|
+
return {
|
|
24
|
+
...stateDef,
|
|
25
|
+
transitions: stateDef.transitions.map(t =>
|
|
26
|
+
typeof t === 'string' ? { to: fn(t) } : { ...t, to: fn(t.to) }
|
|
27
|
+
),
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function withPrefix(fragment, prefix) {
|
|
32
|
+
if (!prefix) return fragment;
|
|
33
|
+
const states = {};
|
|
34
|
+
for (const [name, def] of Object.entries(fragment.states)) {
|
|
35
|
+
const rewritten = mapTargets(def, t => (t === NEXT ? NEXT : prefixState(prefix, t)));
|
|
36
|
+
states[prefixState(prefix, name)] = rewritten;
|
|
37
|
+
}
|
|
38
|
+
return { entry: prefixState(prefix, fragment.entry), states };
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// ── Service-tier motifs ───────────────────────────────────────────────────
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* listen() — idle-style park. Routes to __NEXT__ on a delegated task,
|
|
45
|
+
* otherwise parks waiting for incoming messages that populate ctx flags.
|
|
46
|
+
*/
|
|
47
|
+
export function listen() {
|
|
48
|
+
return {
|
|
49
|
+
entry: 'idle',
|
|
50
|
+
states: {
|
|
51
|
+
idle: {
|
|
52
|
+
transitions: [
|
|
53
|
+
{
|
|
54
|
+
to: NEXT,
|
|
55
|
+
when: ctx =>
|
|
56
|
+
ctx.approval &&
|
|
57
|
+
ctx.taskAssignment &&
|
|
58
|
+
(ctx.taskAssignment.state === 'active' ||
|
|
59
|
+
ctx.taskAssignment.state === 'in-progress') &&
|
|
60
|
+
!!ctx.taskAssignment.parentTaskId,
|
|
61
|
+
},
|
|
62
|
+
{ to: 'listening_hold' },
|
|
63
|
+
],
|
|
64
|
+
},
|
|
65
|
+
listening_hold: {
|
|
66
|
+
// Placeholder: a paired `propose()` replaces this with the real
|
|
67
|
+
// listening state. If compiled alone, stays parked.
|
|
68
|
+
transitions: [{ to: 'idle' }],
|
|
69
|
+
},
|
|
70
|
+
},
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* propose({ prompt, constraints }) — user-facing listening + approval gate.
|
|
76
|
+
*/
|
|
77
|
+
export function propose({ role, goal, context = '', constraints = '' } = {}) {
|
|
78
|
+
return {
|
|
79
|
+
entry: 'listening',
|
|
80
|
+
states: {
|
|
81
|
+
listening: {
|
|
82
|
+
prompt: {
|
|
83
|
+
role: role ?? 'A helpful assistant',
|
|
84
|
+
goal: goal ?? 'Understand the user and propose a task for concrete requests.',
|
|
85
|
+
context,
|
|
86
|
+
constraints:
|
|
87
|
+
constraints ||
|
|
88
|
+
'Respond in the user\'s language. Only propose once per pending request.',
|
|
89
|
+
},
|
|
90
|
+
actions: {
|
|
91
|
+
proposeTask: {
|
|
92
|
+
prompt: {
|
|
93
|
+
instruction:
|
|
94
|
+
'Create a task proposal. Set proposedTo to the human user id from <recipients><users>.',
|
|
95
|
+
format: '{"description": "...", "proposedTo": "<userId>"}',
|
|
96
|
+
when: 'The user has a concrete request and there is no pending delegation.',
|
|
97
|
+
},
|
|
98
|
+
tool: 'task_make_task',
|
|
99
|
+
},
|
|
100
|
+
respondToProposal: {
|
|
101
|
+
prompt: {
|
|
102
|
+
instruction: 'Accept or reject a pending delegation directed at you.',
|
|
103
|
+
when: 'A proposal is pending and directed at you (taskAssignment has parentTaskId).',
|
|
104
|
+
},
|
|
105
|
+
tool: 'task_respond_proposal',
|
|
106
|
+
},
|
|
107
|
+
sendMessage: {
|
|
108
|
+
prompt: {
|
|
109
|
+
instruction: 'Respond directly for greetings or trivial queries.',
|
|
110
|
+
when: 'The request does not need a task.',
|
|
111
|
+
},
|
|
112
|
+
},
|
|
113
|
+
},
|
|
114
|
+
transitions: [
|
|
115
|
+
{ to: 'awaitingApproval', when: ctx => ctx.proposal != null },
|
|
116
|
+
{
|
|
117
|
+
to: NEXT,
|
|
118
|
+
when: ctx =>
|
|
119
|
+
ctx.respondedProposal != null &&
|
|
120
|
+
ctx.respondedProposal.action !== 'reject',
|
|
121
|
+
},
|
|
122
|
+
{ to: 'idle', when: ctx => ctx.messageSent || ctx.activeWait },
|
|
123
|
+
],
|
|
124
|
+
},
|
|
125
|
+
awaitingApproval: {
|
|
126
|
+
transitions: [
|
|
127
|
+
{ to: NEXT, when: ctx => ctx.approval },
|
|
128
|
+
{ to: 'idle', when: ctx => ctx.rejection },
|
|
129
|
+
{ to: 'listening' },
|
|
130
|
+
],
|
|
131
|
+
},
|
|
132
|
+
},
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* execute({ role, goal, context, constraints, toolHint, askWhen }) —
|
|
138
|
+
* the "do the work" state. Calls tools, messages user, closes task. If
|
|
139
|
+
* details are missing, loops through an `ask()`-compatible park state.
|
|
140
|
+
*/
|
|
141
|
+
export function execute({
|
|
142
|
+
role = 'Executing a task',
|
|
143
|
+
goal = 'Use the available tools to complete the task, report to the user, then close the task.',
|
|
144
|
+
context = '',
|
|
145
|
+
constraints = 'Report results to the user before closing the task. Fail with a clear reason on domain mismatch.',
|
|
146
|
+
toolHint,
|
|
147
|
+
name = 'executing',
|
|
148
|
+
} = {}) {
|
|
149
|
+
const parkName = `${name}_await`;
|
|
150
|
+
return {
|
|
151
|
+
entry: name,
|
|
152
|
+
states: {
|
|
153
|
+
[name]: {
|
|
154
|
+
prompt: { role, goal, context, constraints },
|
|
155
|
+
actions: {
|
|
156
|
+
...(toolHint && {
|
|
157
|
+
runTool: {
|
|
158
|
+
prompt: {
|
|
159
|
+
instruction: `Call ${toolHint} to execute the task.`,
|
|
160
|
+
when: 'You have enough details.',
|
|
161
|
+
},
|
|
162
|
+
tool: toolHint,
|
|
163
|
+
},
|
|
164
|
+
}),
|
|
165
|
+
askForDetails: {
|
|
166
|
+
prompt: {
|
|
167
|
+
instruction: 'Ask the user (from <recipients><users>) for the missing details.',
|
|
168
|
+
when: 'The task is underspecified and you have NOT already asked.',
|
|
169
|
+
},
|
|
170
|
+
},
|
|
171
|
+
task_update_task: {
|
|
172
|
+
prompt: {
|
|
173
|
+
instruction:
|
|
174
|
+
'Close the task: status "completed" with a result summary on success, or "failed" with a reason.',
|
|
175
|
+
when: 'After tool execution you are done or must abandon.',
|
|
176
|
+
},
|
|
177
|
+
tool: 'task_update_task',
|
|
178
|
+
},
|
|
179
|
+
sendMessage: {
|
|
180
|
+
prompt: {
|
|
181
|
+
instruction: 'Send the outcome to the user.',
|
|
182
|
+
when: 'You have results or need a quick update.',
|
|
183
|
+
},
|
|
184
|
+
},
|
|
185
|
+
},
|
|
186
|
+
transitions: [
|
|
187
|
+
{ to: NEXT, when: ctx => ctx.taskCompleted || ctx.taskFailed },
|
|
188
|
+
{ to: NEXT, when: ctx => ctx.activeWait },
|
|
189
|
+
{ to: parkName, when: ctx => ctx.messageSent },
|
|
190
|
+
{ to: name, when: ctx => ctx.toolResults?.length > 0 },
|
|
191
|
+
{ to: parkName }, // catch-all park
|
|
192
|
+
],
|
|
193
|
+
},
|
|
194
|
+
[parkName]: {
|
|
195
|
+
transitions: [{ to: name }],
|
|
196
|
+
},
|
|
197
|
+
},
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// ── Orchestrator motifs ───────────────────────────────────────────────────
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* plan({ maxItems, label }) — discovery + prepare_agents stage.
|
|
205
|
+
*/
|
|
206
|
+
export function plan({
|
|
207
|
+
role = 'An orchestrator planning work',
|
|
208
|
+
goal = 'Identify the steps and specialists needed. Use list_agents then prepare_agents.',
|
|
209
|
+
context = '',
|
|
210
|
+
constraints = 'Match agents to steps by domain.',
|
|
211
|
+
maxItems = 5,
|
|
212
|
+
name = 'planning',
|
|
213
|
+
} = {}) {
|
|
214
|
+
return {
|
|
215
|
+
entry: name,
|
|
216
|
+
states: {
|
|
217
|
+
[name]: {
|
|
218
|
+
prompt: {
|
|
219
|
+
role,
|
|
220
|
+
goal,
|
|
221
|
+
context: context || `Up to ${maxItems} items.`,
|
|
222
|
+
constraints,
|
|
223
|
+
},
|
|
224
|
+
actions: {
|
|
225
|
+
list_agents: {
|
|
226
|
+
prompt: {
|
|
227
|
+
instruction: 'Search for specialists by domain.',
|
|
228
|
+
when: 'You need to find agents.',
|
|
229
|
+
},
|
|
230
|
+
tool: 'list_agents',
|
|
231
|
+
},
|
|
232
|
+
prepare_agents: {
|
|
233
|
+
prompt: {
|
|
234
|
+
instruction: 'Wake the chosen agents.',
|
|
235
|
+
when: 'You have identified all agents needed.',
|
|
236
|
+
},
|
|
237
|
+
tool: 'prepare_agents',
|
|
238
|
+
},
|
|
239
|
+
sendMessage: {
|
|
240
|
+
prompt: {
|
|
241
|
+
instruction: 'Explain the plan to the user.',
|
|
242
|
+
when: 'You have a plan ready.',
|
|
243
|
+
},
|
|
244
|
+
},
|
|
245
|
+
},
|
|
246
|
+
transitions: [
|
|
247
|
+
{
|
|
248
|
+
to: NEXT,
|
|
249
|
+
when: ctx => ctx.toolResults?.some(r => r.name === 'prepare_agents'),
|
|
250
|
+
},
|
|
251
|
+
{ to: name, when: ctx => ctx.toolResults?.length > 0 },
|
|
252
|
+
{ to: NEXT, when: ctx => ctx.messageSent },
|
|
253
|
+
],
|
|
254
|
+
},
|
|
255
|
+
},
|
|
256
|
+
};
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* dispatch({ parallel, label }) — send subtasks to workers.
|
|
261
|
+
*/
|
|
262
|
+
export function dispatch({
|
|
263
|
+
parallel = false,
|
|
264
|
+
role,
|
|
265
|
+
goal,
|
|
266
|
+
context = '',
|
|
267
|
+
constraints,
|
|
268
|
+
name = parallel ? 'mapping' : 'delegating',
|
|
269
|
+
} = {}) {
|
|
270
|
+
return {
|
|
271
|
+
entry: name,
|
|
272
|
+
states: {
|
|
273
|
+
[name]: {
|
|
274
|
+
prompt: {
|
|
275
|
+
role: role ?? (parallel ? 'Dispatching parallel work' : 'Executing steps sequentially'),
|
|
276
|
+
goal: goal ?? (parallel
|
|
277
|
+
? 'Send all chunks in a single dispatch_to_agents call.'
|
|
278
|
+
: 'Send the current step. Include prior results as context.'),
|
|
279
|
+
context,
|
|
280
|
+
constraints: constraints ?? (parallel
|
|
281
|
+
? 'Dispatch all in one call. Each chunk must stand alone.'
|
|
282
|
+
: 'One step at a time. Include previous step outputs.'),
|
|
283
|
+
},
|
|
284
|
+
actions: {
|
|
285
|
+
dispatch_to_agents: {
|
|
286
|
+
prompt: {
|
|
287
|
+
instruction: parallel
|
|
288
|
+
? 'Send all chunks to their agents in parallel.'
|
|
289
|
+
: 'Send the current step to the specialist.',
|
|
290
|
+
when: 'Ready to execute.',
|
|
291
|
+
},
|
|
292
|
+
tool: 'dispatch_to_agents',
|
|
293
|
+
},
|
|
294
|
+
sendMessage: {
|
|
295
|
+
prompt: { instruction: 'Update the user on progress.', when: 'A step completed.' },
|
|
296
|
+
},
|
|
297
|
+
},
|
|
298
|
+
transitions: [
|
|
299
|
+
{
|
|
300
|
+
to: NEXT,
|
|
301
|
+
when: ctx => ctx.toolResults?.some(r => r.name === 'dispatch_to_agents'),
|
|
302
|
+
},
|
|
303
|
+
{ to: NEXT },
|
|
304
|
+
],
|
|
305
|
+
},
|
|
306
|
+
},
|
|
307
|
+
};
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
/**
|
|
311
|
+
* awaitSubtasks({ mode }) — park until subtaskResult(s) arrive.
|
|
312
|
+
* mode='one' (pipeline step), mode='all' (map-reduce / voting).
|
|
313
|
+
*/
|
|
314
|
+
export function awaitSubtasks({ mode = 'one', name = 'awaitingSubtasks' } = {}) {
|
|
315
|
+
return {
|
|
316
|
+
entry: name,
|
|
317
|
+
states: {
|
|
318
|
+
[name]: {
|
|
319
|
+
transitions: [
|
|
320
|
+
{ to: NEXT, when: ctx => ctx.subtaskResult },
|
|
321
|
+
{ to: NEXT, when: ctx => ctx.subtaskFailed },
|
|
322
|
+
...(mode === 'all'
|
|
323
|
+
? [{ to: name }]
|
|
324
|
+
: [{ to: NEXT }]),
|
|
325
|
+
],
|
|
326
|
+
},
|
|
327
|
+
},
|
|
328
|
+
};
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
/**
|
|
332
|
+
* synthesize({ role, goal }) — merge + close task.
|
|
333
|
+
*/
|
|
334
|
+
export function synthesize({
|
|
335
|
+
role = 'Synthesizing results',
|
|
336
|
+
goal = 'Merge results into a coherent final answer, present to user, then close the task.',
|
|
337
|
+
context = '',
|
|
338
|
+
constraints = 'Synthesize — do not concatenate. Dedupe. Resolve conflicts explicitly.',
|
|
339
|
+
name = 'synthesizing',
|
|
340
|
+
} = {}) {
|
|
341
|
+
return {
|
|
342
|
+
entry: name,
|
|
343
|
+
states: {
|
|
344
|
+
[name]: {
|
|
345
|
+
prompt: { role, goal, context, constraints },
|
|
346
|
+
actions: {
|
|
347
|
+
sendMessage: {
|
|
348
|
+
prompt: {
|
|
349
|
+
instruction: 'Present the final result to the user.',
|
|
350
|
+
when: 'You have everything merged.',
|
|
351
|
+
},
|
|
352
|
+
},
|
|
353
|
+
task_update_task: {
|
|
354
|
+
prompt: {
|
|
355
|
+
instruction: 'Close the task with the final result.',
|
|
356
|
+
when: 'After presenting the result.',
|
|
357
|
+
},
|
|
358
|
+
tool: 'task_update_task',
|
|
359
|
+
},
|
|
360
|
+
},
|
|
361
|
+
transitions: [
|
|
362
|
+
{ to: NEXT, when: ctx => ctx.taskCompleted || ctx.taskFailed },
|
|
363
|
+
{ to: NEXT, when: ctx => ctx.messageSent || ctx.activeWait },
|
|
364
|
+
],
|
|
365
|
+
},
|
|
366
|
+
},
|
|
367
|
+
};
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
// ── Composition ───────────────────────────────────────────────────────────
|
|
371
|
+
|
|
372
|
+
/**
|
|
373
|
+
* seq(...fragments) — chain fragments: each fragment's __NEXT__ rewrites to
|
|
374
|
+
* the next fragment's entry. Last fragment's __NEXT__ is left unresolved
|
|
375
|
+
* (compile() will rewrite to `end`).
|
|
376
|
+
*/
|
|
377
|
+
export function seq(...fragments) {
|
|
378
|
+
if (!fragments.length) throw new Error('seq() needs at least one fragment');
|
|
379
|
+
const states = {};
|
|
380
|
+
for (let i = 0; i < fragments.length; i++) {
|
|
381
|
+
const frag = fragments[i];
|
|
382
|
+
const nextEntry = i + 1 < fragments.length ? fragments[i + 1].entry : NEXT;
|
|
383
|
+
for (const [name, def] of Object.entries(frag.states)) {
|
|
384
|
+
states[name] = mapTargets(def, t => (t === NEXT ? nextEntry : t));
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
return { entry: fragments[0].entry, states };
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
/**
|
|
391
|
+
* loop(n, ...fragments) — repeat a sub-sequence n times. Each iteration is
|
|
392
|
+
* state-prefixed so states don't collide. Exit after iteration n.
|
|
393
|
+
*/
|
|
394
|
+
export function loop(n, ...fragments) {
|
|
395
|
+
if (n < 1) throw new Error('loop() needs n >= 1');
|
|
396
|
+
const iterations = [];
|
|
397
|
+
for (let i = 0; i < n; i++) {
|
|
398
|
+
iterations.push(withPrefix(seq(...fragments), `i${i}`));
|
|
399
|
+
}
|
|
400
|
+
return seq(...iterations);
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
/**
|
|
404
|
+
* compile(fragment, { end }) — rewrite trailing __NEXT__ to `end` (default
|
|
405
|
+
* 'idle') and return { initial, states }.
|
|
406
|
+
*/
|
|
407
|
+
export function compile(fragment, { end = 'idle' } = {}) {
|
|
408
|
+
const states = {};
|
|
409
|
+
for (const [name, def] of Object.entries(fragment.states)) {
|
|
410
|
+
states[name] = mapTargets(def, t => (t === NEXT ? end : t));
|
|
411
|
+
}
|
|
412
|
+
// Ensure an `idle` terminal exists if `end` is 'idle' and absent.
|
|
413
|
+
if (end === 'idle' && !states.idle) {
|
|
414
|
+
states.idle = { transitions: [{ to: fragment.entry }] };
|
|
415
|
+
}
|
|
416
|
+
return { initial: 'idle', states };
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
// Convenience: the ubiquitous "service agent" shape in one call.
|
|
420
|
+
export function service({
|
|
421
|
+
role,
|
|
422
|
+
goal,
|
|
423
|
+
context,
|
|
424
|
+
constraints,
|
|
425
|
+
toolHint,
|
|
426
|
+
workName = 'executing',
|
|
427
|
+
proposeContext,
|
|
428
|
+
proposeConstraints,
|
|
429
|
+
} = {}) {
|
|
430
|
+
return seq(
|
|
431
|
+
listen(),
|
|
432
|
+
propose({ role, goal, context: proposeContext ?? context, constraints: proposeConstraints }),
|
|
433
|
+
execute({ role, goal: undefined, context, constraints, toolHint, name: workName }),
|
|
434
|
+
);
|
|
435
|
+
}
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
import { Agent } from '../agent/Agent.js';
|
|
2
|
+
import { ToolManager } from '../tools/ToolManager.js';
|
|
3
|
+
import { TASK_PROTOCOL_TOOLS } from '../tasks/taskProtocolTools.js';
|
|
4
|
+
import { OpenAIAdapter } from '../adapters/OpenAIAdapter.js';
|
|
5
|
+
import { WebSocketClient, ContextReader, ContextWriter, TelemetryClient } from '@ziggs-ai/api-client';
|
|
6
|
+
import { PromptBuilder } from '../cognition/PromptBuilder.js';
|
|
7
|
+
import { TaskService } from '../tasks/TaskService.js';
|
|
8
|
+
import { ConnectionPool } from '../ConnectionPool.js';
|
|
9
|
+
|
|
10
|
+
function createToolManager(options) {
|
|
11
|
+
const toolManager = new ToolManager();
|
|
12
|
+
const userTools = options.tools || options.services?.tools || [];
|
|
13
|
+
const taskTools = options.taskTools !== false ? TASK_PROTOCOL_TOOLS : [];
|
|
14
|
+
toolManager.registerAll([...taskTools, ...(Array.isArray(userTools) ? userTools : [])]);
|
|
15
|
+
return toolManager;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function createServices(options) {
|
|
19
|
+
const toolManager = createToolManager(options);
|
|
20
|
+
const llm = new OpenAIAdapter({ key: options.openaiKey, model: options.services?.llm?.model || options.model });
|
|
21
|
+
const contextReader = new ContextReader(options.operatorKey, options.agentId);
|
|
22
|
+
const contextWriter = new ContextWriter(options.operatorKey, options.agentId);
|
|
23
|
+
const promptBuilder = new PromptBuilder({ description: options.description, specialization: options.specialization });
|
|
24
|
+
const taskService = new TaskService(options.operatorKey, options.agentId);
|
|
25
|
+
const telemetryClient = new TelemetryClient(options.operatorKey, options.agentId);
|
|
26
|
+
return { toolManager, llm, contextReader, contextWriter, promptBuilder, taskService, telemetryClient };
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export class ZiggsAgent {
|
|
30
|
+
constructor(options = {}) {
|
|
31
|
+
this.options = {
|
|
32
|
+
openaiKey: options.openaiKey,
|
|
33
|
+
model: options.model || options.services?.llm?.model || 'gpt-4o-mini',
|
|
34
|
+
operatorKey: options.operatorKey || process.env.ZIGGS_OPERATOR_KEY || null,
|
|
35
|
+
agentId: options.agentId || null,
|
|
36
|
+
name: options.name || null,
|
|
37
|
+
wsUrl: options.wsUrl,
|
|
38
|
+
description: options.description || 'A helpful assistant',
|
|
39
|
+
specialization: options.specialization || null,
|
|
40
|
+
tools: options.tools || [],
|
|
41
|
+
taskTools: options.taskTools,
|
|
42
|
+
states: options.states || null,
|
|
43
|
+
initial: options.initial || null,
|
|
44
|
+
services: options.services || null,
|
|
45
|
+
workflow: options.workflow || null,
|
|
46
|
+
};
|
|
47
|
+
this._validate();
|
|
48
|
+
this._setup();
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
_validate() {
|
|
52
|
+
if (!this.options.openaiKey) throw new Error('openaiKey is required');
|
|
53
|
+
if (!this.options.operatorKey) {
|
|
54
|
+
throw new Error(
|
|
55
|
+
'operatorKey is required — pass { operatorKey } or set ZIGGS_OPERATOR_KEY. ' +
|
|
56
|
+
'Mint one via Agents/scripts/mint-operator-key.js (scope: agents:impersonate).',
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
if (!this.options.agentId) {
|
|
60
|
+
throw new Error('agentId is required — every agent must declare its stable kebab-case agentId in its config.');
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
_setup() {
|
|
65
|
+
const services = createServices(this.options);
|
|
66
|
+
this.toolManager = services.toolManager;
|
|
67
|
+
this.llm = services.llm;
|
|
68
|
+
|
|
69
|
+
this.wsClient = new WebSocketClient({
|
|
70
|
+
wsUrl: this.options.wsUrl,
|
|
71
|
+
operatorKey: this.options.operatorKey,
|
|
72
|
+
agentId: this.options.agentId,
|
|
73
|
+
label: this.options.name || 'agent',
|
|
74
|
+
name: this.options.name || null,
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
const messageSender = (message, receiverId, chatId) => {
|
|
78
|
+
this.wsClient.sendResponse(message, { receiverId, chatId });
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
let definition = this.options.workflow;
|
|
82
|
+
if (!definition && this.options.states) {
|
|
83
|
+
definition = {
|
|
84
|
+
id: this.options.agentId,
|
|
85
|
+
description: this.options.description,
|
|
86
|
+
initial: this.options.initial || 'idle',
|
|
87
|
+
states: this.options.states,
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
if (!definition?.states) {
|
|
91
|
+
throw new Error('ZiggsAgent: states definition or workflow is required');
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
this.agent = new Agent({
|
|
95
|
+
llm: services.llm,
|
|
96
|
+
toolManager: services.toolManager,
|
|
97
|
+
contextReader: services.contextReader,
|
|
98
|
+
contextWriter: services.contextWriter,
|
|
99
|
+
taskService: services.taskService,
|
|
100
|
+
messageSender,
|
|
101
|
+
promptBuilder: services.promptBuilder,
|
|
102
|
+
operatorKey: this.options.operatorKey,
|
|
103
|
+
agentId: this.options.agentId,
|
|
104
|
+
workflow: definition,
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
this.wsClient.setMessageHandler(async (text, metadata) => {
|
|
108
|
+
await this.agent.handleMessage(text, metadata);
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
connect() { this.wsClient.connect(); return this; }
|
|
113
|
+
connectAsync(timeout) { return this.wsClient.connectAsync(timeout); }
|
|
114
|
+
disconnect() { this.wsClient.disconnect(); }
|
|
115
|
+
registerTool(tool) { return this.toolManager.register(tool); }
|
|
116
|
+
async handleMessage(text, metadata = {}) { return await this.agent.handleMessage(text, metadata); }
|
|
117
|
+
isConnected() { return this.wsClient.isConnected(); }
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Factory for a single agent or a fleet.
|
|
122
|
+
*
|
|
123
|
+
* createAgent(config) → ZiggsAgent (hello-world / single agent)
|
|
124
|
+
* createAgent([cfg1, cfg2]) → ConnectionPool (any fleet — lazy, one control socket)
|
|
125
|
+
*
|
|
126
|
+
* Pool agents stay disconnected until `pool.wake(id)` — or until the backend
|
|
127
|
+
* pushes a `launcher:wake` frame over the control socket opened by
|
|
128
|
+
* `pool.startControl({ wsUrl, operatorKey })`. For the full boot sequence
|
|
129
|
+
* (control socket, health server, orchestrator, SIGTERM), use `runLauncher`.
|
|
130
|
+
*
|
|
131
|
+
* @param {object|object[]} options
|
|
132
|
+
* @param {object} [opts]
|
|
133
|
+
* @param {object} [opts.poolOptions] Forwarded to `new ConnectionPool(...)` when options is an array
|
|
134
|
+
*/
|
|
135
|
+
export function createAgent(options = {}, { poolOptions } = {}) {
|
|
136
|
+
if (Array.isArray(options)) {
|
|
137
|
+
const pool = new ConnectionPool(poolOptions);
|
|
138
|
+
pool.register(options);
|
|
139
|
+
return pool;
|
|
140
|
+
}
|
|
141
|
+
return new ZiggsAgent(options);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const DEFAULT_WAIT_PROMPT = Object.freeze({
|
|
145
|
+
instruction: 'Do nothing - wait for the next event.',
|
|
146
|
+
when: 'No action is needed, you are waiting for external input, or you should pause without repeating yourself.',
|
|
147
|
+
format: '{"thought": "...", "action": "wait"}',
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
function injectWaitActions(states, initial) {
|
|
151
|
+
for (const [, stateDef] of Object.entries(states)) {
|
|
152
|
+
if (!stateDef.prompt || !stateDef.actions) continue;
|
|
153
|
+
if (!stateDef.actions.wait) {
|
|
154
|
+
stateDef.actions.wait = { prompt: { ...DEFAULT_WAIT_PROMPT } };
|
|
155
|
+
}
|
|
156
|
+
if (Array.isArray(stateDef.transitions)) {
|
|
157
|
+
const checksActiveWait = stateDef.transitions.some(
|
|
158
|
+
d => typeof d !== 'string' && d.when && /activeWait/.test(d.when.toString())
|
|
159
|
+
);
|
|
160
|
+
if (!checksActiveWait) {
|
|
161
|
+
stateDef.transitions.push({ to: initial, when: ctx => ctx.activeWait });
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
export function defineAgent({
|
|
168
|
+
agentId, description, specialization, services: agentServices,
|
|
169
|
+
initial, states, tools = [], operatorKey, ...rest
|
|
170
|
+
}) {
|
|
171
|
+
if (!agentId) throw new Error('[defineAgent] agentId is required (stable kebab-case, same id registered with backend)');
|
|
172
|
+
if (!description) throw new Error('[defineAgent] description is required');
|
|
173
|
+
if (!states) throw new Error('[defineAgent] states is required');
|
|
174
|
+
if (!initial) throw new Error('[defineAgent] initial state is required');
|
|
175
|
+
|
|
176
|
+
injectWaitActions(states, initial);
|
|
177
|
+
|
|
178
|
+
return {
|
|
179
|
+
openaiKey: process.env.OPENAI_API_KEY,
|
|
180
|
+
...(operatorKey && { operatorKey }),
|
|
181
|
+
agentId,
|
|
182
|
+
description,
|
|
183
|
+
...(specialization && { specialization }),
|
|
184
|
+
tools: [...tools, ...(agentServices?.tools || [])],
|
|
185
|
+
model: agentServices?.llm?.model || process.env.OPENAI_MODEL || 'gpt-4o-mini',
|
|
186
|
+
workflow: { id: agentId, description, initial, states },
|
|
187
|
+
services: agentServices,
|
|
188
|
+
...rest,
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
export default ZiggsAgent;
|