@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.
Files changed (40) hide show
  1. package/README.md +82 -0
  2. package/package.json +26 -0
  3. package/src/ConnectionPool.js +133 -0
  4. package/src/adapters/OpenAIAdapter.js +73 -0
  5. package/src/adapters/index.js +1 -0
  6. package/src/agent/Agent.js +121 -0
  7. package/src/agent/EventQueue.js +68 -0
  8. package/src/agent/OutboxBuffer.js +62 -0
  9. package/src/cognition/PromptBuilder.js +312 -0
  10. package/src/cognition/resolveActionTool.js +12 -0
  11. package/src/cognition/runTurn.js +578 -0
  12. package/src/context/applyEffects.js +133 -0
  13. package/src/context/batch.js +25 -0
  14. package/src/context/classifyEnvelope.js +82 -0
  15. package/src/context/routingLabels.js +54 -0
  16. package/src/createHealthServer.js +28 -0
  17. package/src/formatters/HistoryFormatter.js +257 -0
  18. package/src/formatters/TaskFormatter.js +180 -0
  19. package/src/formatters/index.js +9 -0
  20. package/src/index.js +76 -0
  21. package/src/ingress/normalizeIncoming.js +70 -0
  22. package/src/runLauncher.js +159 -0
  23. package/src/shared/ids.js +7 -0
  24. package/src/shared/types.js +86 -0
  25. package/src/tasks/TaskService.js +247 -0
  26. package/src/tasks/index.js +9 -0
  27. package/src/tasks/taskCore.js +229 -0
  28. package/src/tasks/taskProtocolRegistry.js +22 -0
  29. package/src/tasks/taskProtocolRunner.js +107 -0
  30. package/src/tasks/taskProtocolTools.js +87 -0
  31. package/src/tools/ToolManager.js +79 -0
  32. package/src/tools/ToolProvider.js +29 -0
  33. package/src/tools/defineTool.js +82 -0
  34. package/src/tools/index.js +11 -0
  35. package/src/utils/jsonExtractor.js +139 -0
  36. package/src/workflow/AgentMachine.js +250 -0
  37. package/src/workflow/WorkflowRuntime.js +63 -0
  38. package/src/workflow/dsl.js +287 -0
  39. package/src/workflow/motifs.js +435 -0
  40. package/src/ziggs/runtime.js +192 -0
@@ -0,0 +1,578 @@
1
+ /**
2
+ * Single LLM turn for a thinking state: read context → prompt → LLM → one action → effects.
3
+ */
4
+
5
+ import { ContextProperties } from '../shared/types.js';
6
+ import { generateId } from '../shared/ids.js';
7
+ import { safeParseJSON } from '../utils/jsonExtractor.js';
8
+ import { buildContextUpdates } from '../context/applyEffects.js';
9
+ import { resolveActionTool } from './resolveActionTool.js';
10
+
11
+ const MAX_TOOL_LOOP_ITERS = 20;
12
+
13
+ const TASK_CREATION_TOOLS = new Set(['task_make_task', 'task_make_sub_tasks']);
14
+
15
+ /**
16
+ * When the current workflow state has no action bound to this tool (e.g. agent_network in delegating),
17
+ * still execute — otherwise the model's tool call is dropped and delegation cannot recover.
18
+ */
19
+ const GENERIC_FALLBACK_TOOLS = new Set(['agent_network', 'task_board']);
20
+
21
+ /**
22
+ * Per assistant message: only one task-creation call; only one task_update_task per taskId.
23
+ * Models often emit duplicate task_update_task after a successful close → avoid double PATCH / errors.
24
+ */
25
+ function annotateToolCallDedup(toolCalls) {
26
+ let sawTaskCreate = false;
27
+ const seenUpdateTaskIds = new Set();
28
+ return (toolCalls || []).map(tc => {
29
+ const name = tc?.function?.name;
30
+ if (name && TASK_CREATION_TOOLS.has(name)) {
31
+ if (sawTaskCreate) {
32
+ return {
33
+ tc,
34
+ skipExecute: true,
35
+ skipReason:
36
+ 'duplicate task creation in same assistant message; only the first call was executed',
37
+ };
38
+ }
39
+ sawTaskCreate = true;
40
+ return { tc, skipExecute: false };
41
+ }
42
+ if (name === 'task_update_task') {
43
+ let taskId = null;
44
+ try {
45
+ const a = JSON.parse(tc.function.arguments || '{}');
46
+ taskId = typeof a.taskId === 'string' ? a.taskId : null;
47
+ } catch {}
48
+ const key = taskId || tc.id || '_';
49
+ if (seenUpdateTaskIds.has(key)) {
50
+ return {
51
+ tc,
52
+ skipExecute: true,
53
+ skipReason:
54
+ 'duplicate task_update_task for the same taskId in one assistant message; only the first call was executed',
55
+ };
56
+ }
57
+ seenUpdateTaskIds.add(key);
58
+ return { tc, skipExecute: false };
59
+ }
60
+ return { tc, skipExecute: false };
61
+ });
62
+ }
63
+
64
+ function isTerminalPartial(partial) {
65
+ return Boolean(
66
+ partial?.delegatedTask ||
67
+ partial?.messageSent ||
68
+ partial?.activeWait ||
69
+ partial?.proposal,
70
+ );
71
+ }
72
+
73
+ function pickPrimaryUserIdFromContext(context) {
74
+ const users = context?.[ContextProperties.USERS] || [];
75
+ for (const u of users) {
76
+ const id = u?.userId || u?.id;
77
+ if (id && typeof id === 'string') return id;
78
+ }
79
+ const hist = context?.[ContextProperties.HISTORY] || [];
80
+ for (let i = hist.length - 1; i >= 0; i--) {
81
+ const row = hist[i];
82
+ const type = String(row?.sender?.type || row?.senderType || '').toUpperCase();
83
+ const id = row?.sender?.id ?? row?.senderId;
84
+ if (type === 'USER' && id) return id;
85
+ }
86
+ return null;
87
+ }
88
+
89
+ function shouldEchoToolSummaryToUser(result) {
90
+ if (!result || typeof result !== 'object') return false;
91
+ if (typeof result.message !== 'string' || !result.message.trim()) return false;
92
+ if (result.success === false) return false;
93
+ if (result.error) return false;
94
+ return true;
95
+ }
96
+
97
+ /**
98
+ * Tools may set `echoUserSummaryOnSuccess` (via defineTool 4th arg). If the model sent no
99
+ * chat this turn, mirror the tool's `result.message` to the user once (last matching tool wins).
100
+ */
101
+ async function echoOptInToolSummaryIfNeeded({
102
+ allEmittedEvents,
103
+ messageSender,
104
+ chatId,
105
+ context,
106
+ toolManager,
107
+ }) {
108
+ if (!messageSender || !chatId || !context || !toolManager?.getTool) return;
109
+ if (allEmittedEvents.some(e => e?.type === 'message_sent')) return;
110
+
111
+ let summary = null;
112
+ let toolName = null;
113
+ for (const ev of allEmittedEvents) {
114
+ if (ev?.type !== 'tool_result' || !ev.tool) continue;
115
+ const reg = toolManager.getTool(ev.tool);
116
+ if (!reg?.echoUserSummaryOnSuccess) continue;
117
+ if (!shouldEchoToolSummaryToUser(ev.result)) continue;
118
+ summary = ev.result.message.trim();
119
+ toolName = ev.tool;
120
+ }
121
+ if (!summary) return;
122
+
123
+ const receiverId = pickPrimaryUserIdFromContext(context);
124
+ if (!receiverId) return;
125
+
126
+ const max = 3500;
127
+ const safe = summary.length > max ? `${summary.slice(0, max)}\n\n[truncated]` : summary;
128
+ try {
129
+ await messageSender(safe, receiverId, chatId);
130
+ } catch (e) {
131
+ console.warn(`[runTurn] echoUserSummaryOnSuccess send failed: ${e?.message || e}`);
132
+ return;
133
+ }
134
+ console.log(`[runTurn] echoed result.message (echoUserSummaryOnSuccess, tool=${toolName})`);
135
+ allEmittedEvents.push({
136
+ type: 'message_sent',
137
+ receiverId,
138
+ chatId,
139
+ message: safe,
140
+ senderId: 'system',
141
+ });
142
+ }
143
+
144
+ export async function runTurn(input) {
145
+ const {
146
+ stateId, statePrompt, actions,
147
+ chatId, incomingEvent, machineContext,
148
+ _services, _definition,
149
+ } = input;
150
+
151
+ const {
152
+ llm, toolManager,
153
+ contextReader, contextWriter, taskService,
154
+ messageSender, messageBuffer, promptBuilder,
155
+ operatorKey, agentId,
156
+ } = _services;
157
+
158
+ console.log(`[runTurn] enter state=${stateId}`);
159
+
160
+ const serverContext = await readServerContext({
161
+ chatId, contextReader, toolManager, messageBuffer, incomingEvent,
162
+ });
163
+
164
+ const prompt = buildDecisionPrompt({
165
+ statePrompt, actions, serverContext, incomingEvent, definition: _definition,
166
+ promptBuilder, chatId, stateId, machineContext,
167
+ });
168
+
169
+ // Build initial message array for the agentic loop.
170
+ // When tools are available let the model use tool_calls; only force JSON when there are no tools.
171
+ const systemContent = serverContext.tools.length > 0
172
+ ? 'You are a helpful AI agent. Use the available tools to complete your tasks. When you are finished using tools, respond with a valid JSON decision object.'
173
+ : 'Respond with valid JSON.';
174
+ const messages = [
175
+ { role: 'system', content: systemContent },
176
+ { role: 'user', content: prompt },
177
+ ];
178
+
179
+ const allEmittedEvents = [];
180
+ let lastAction = null;
181
+ let llmCallsTotal = 0;
182
+ let tokensTotal = 0, tokensInput = 0, tokensOutput = 0;
183
+ const actionNames = Object.keys(actions);
184
+ const turnScratch = {};
185
+ const execServices = {
186
+ messageSender, toolManager, contextWriter, taskService, operatorKey, agentId, chatId,
187
+ agents: serverContext.context?.[ContextProperties.AGENTS] || [],
188
+ turnScratch,
189
+ };
190
+
191
+ console.log(`[runTurn] calling LLM... tools=${serverContext.tools.length}`, serverContext.tools.map(t => t.schema?.function?.name || t.name || 'unnamed'));
192
+
193
+ for (let iter = 0; iter < MAX_TOOL_LOOP_ITERS; iter++) {
194
+ console.log(`[runTurn] loop iter=${iter} start msgCount=${messages.length}`);
195
+ const llmResponse = await llm.chatMessages(messages, serverContext.tools);
196
+ llmCallsTotal++;
197
+ console.log(`[runTurn] loop iter=${iter} llm responded tool_calls=${!!llmResponse?.tool_calls?.length} content=${!!llmResponse?.content}`);
198
+ const usage = llmResponse?.usage;
199
+ tokensTotal += (usage?.total_tokens ?? usage?.totalTokens) || 0;
200
+ tokensInput += (usage?.prompt_tokens ?? usage?.promptTokens) || 0;
201
+ tokensOutput += (usage?.completion_tokens ?? usage?.completionTokens) || 0;
202
+
203
+ if (llmResponse?.tool_calls?.length) {
204
+ // Record LLM reasoning text (content alongside tool_calls) as a thought.
205
+ // This is what produces "many thinkings in a row" — one per loop iteration.
206
+ if (llmResponse.content && contextWriter && chatId) {
207
+ try { await contextWriter.recordThought(chatId, llmResponse.content); } catch {}
208
+ }
209
+
210
+ // — tool call path — execute in assistant-message order (turnScratch + propose→search ordering)
211
+ const tcs = llmResponse.tool_calls;
212
+ if (tcs.length > 1) {
213
+ console.log(`[runTurn] iter=${iter} executing ${tcs.length} tool_calls sequentially`);
214
+ }
215
+
216
+ const annotated = annotateToolCallDedup(tcs);
217
+ const dupCount = annotated.filter(a => a.skipExecute).length;
218
+ if (dupCount > 0) {
219
+ console.warn(
220
+ `[runTurn] iter=${iter} skipping ${dupCount} duplicate tool call(s) in same message (task create/update dedup)`,
221
+ );
222
+ }
223
+
224
+ // Append assistant message with all tool_calls
225
+ messages.push({ role: 'assistant', tool_calls: tcs });
226
+
227
+ const toolCallResults = [];
228
+ for (const ann of annotated) {
229
+ const { tc, skipExecute, skipReason } = ann;
230
+ if (skipExecute) {
231
+ toolCallResults.push({ tc, matchingAction: null, result: null, skippedDuplicate: true, skipReason });
232
+ continue;
233
+ }
234
+ const toolName = tc?.function?.name;
235
+ if (!toolName) {
236
+ toolCallResults.push({ tc, matchingAction: null, result: null });
237
+ continue;
238
+ }
239
+
240
+ let matchingAction = findActionForTool(toolName, actionNames, actions);
241
+ let actionDef = matchingAction ? actions[matchingAction] : null;
242
+
243
+ if (!matchingAction && GENERIC_FALLBACK_TOOLS.has(toolName) && toolManager.getTool?.(toolName)) {
244
+ matchingAction = '_genericDiscoveryTool';
245
+ actionDef = null;
246
+ console.log(
247
+ `[runTurn] iter=${iter} tool "${toolName}" — no state action bound; executing as discovery helper`,
248
+ );
249
+ } else if (!matchingAction) {
250
+ console.log(`[runTurn] iter=${iter} tool "${toolName}" not bound to any action — skipping`);
251
+ toolCallResults.push({ tc, matchingAction: null, result: null });
252
+ continue;
253
+ }
254
+
255
+ let args = {};
256
+ try { args = JSON.parse(tc.function.arguments || '{}'); } catch {}
257
+ const decision = { action: matchingAction, tool: toolName, args, thought: `Using tool: ${toolName}` };
258
+
259
+ console.log(`[runTurn] iter=${iter} action=${matchingAction} (tool_call_id=${tc.id})`);
260
+ const result = await executeDecision(decision, actionDef, execServices);
261
+ toolCallResults.push({ tc, matchingAction, result });
262
+ }
263
+
264
+ // Add tool response messages for ALL tool_calls (required by OpenAI)
265
+ let hasTerminal = false;
266
+ for (const item of toolCallResults) {
267
+ const { tc, matchingAction, result, skippedDuplicate, skipReason } = item;
268
+ if (skippedDuplicate) {
269
+ messages.push({
270
+ role: 'tool',
271
+ tool_call_id: tc.id,
272
+ content: JSON.stringify({ skipped: true, reason: skipReason || 'deduplicated' }),
273
+ });
274
+ continue;
275
+ }
276
+ if (!matchingAction || !result) {
277
+ messages.push({ role: 'tool', tool_call_id: tc.id, content: '{"error":"unknown tool or no result"}' });
278
+ continue;
279
+ }
280
+
281
+ const events = Array.isArray(result) ? result : [result];
282
+ allEmittedEvents.push(...events);
283
+ lastAction = matchingAction;
284
+
285
+ const firstEvent = events[0] || {};
286
+ const toolResultContent = JSON.stringify(firstEvent.result ?? firstEvent);
287
+ messages.push({ role: 'tool', tool_call_id: tc.id, content: toolResultContent });
288
+
289
+ const partial = buildContextUpdates(matchingAction, events);
290
+ if (isTerminalPartial(partial)) {
291
+ hasTerminal = true;
292
+ }
293
+ }
294
+
295
+ if (hasTerminal) {
296
+ console.log(`[runTurn] iter=${iter} terminal event detected, ending loop`);
297
+ break;
298
+ }
299
+ console.log(`[runTurn] iter=${iter} all tool_calls processed — continuing loop`);
300
+ // Remind the LLM to output a JSON decision now that tools have run.
301
+ // This user-role injection is more reliable than the system message alone
302
+ // because it appears as the most recent instruction in the message history.
303
+ messages.push({ role: 'user', content: 'Tools done. Return your JSON decision now (follow the decision_schema). Do not write prose.' });
304
+
305
+ } else {
306
+ // — non-tool (JSON) response path —
307
+ const decision = parseLLMDecision(llmResponse, actionNames, actions);
308
+ if (!decision) {
309
+ console.log(`[runTurn] iter=${iter} no parseable decision`);
310
+ break;
311
+ }
312
+ const chosenAction = decision.action;
313
+ const actionDef = actions[chosenAction];
314
+ applyActionToolBinding(decision, actionDef);
315
+ console.log(`[runTurn] iter=${iter} action=${chosenAction}`);
316
+ lastAction = chosenAction;
317
+
318
+ const result = await executeDecision(decision, actionDef, execServices);
319
+ if (result) {
320
+ const events = Array.isArray(result) ? result : [result];
321
+ allEmittedEvents.push(...events);
322
+
323
+ // If the action had a tool binding, inject the result and continue the loop
324
+ // (LLM responded with JSON instead of tool_calls due to prompt wording)
325
+ const boundTool = resolveActionTool(actionDef, chosenAction);
326
+ if (boundTool) {
327
+ const firstEvent = events[0] || {};
328
+ const toolResultContent = JSON.stringify(firstEvent.result ?? firstEvent);
329
+ messages.push({ role: 'assistant', content: llmResponse.content });
330
+ messages.push({ role: 'user', content: `Tool "${boundTool}" result:\n${toolResultContent}\n\nContinue with your next action.` });
331
+
332
+ const partial = buildContextUpdates(chosenAction, events);
333
+ console.log(`[runTurn] iter=${iter} (json-tool) terminal check: delegatedTask=${!!partial.delegatedTask} messageSent=${!!partial.messageSent} activeWait=${!!partial.activeWait} proposal=${!!partial.proposal}`);
334
+ if (isTerminalPartial(partial)) {
335
+ console.log(`[runTurn] iter=${iter} terminal event detected, ending loop`);
336
+ break;
337
+ }
338
+ console.log(`[runTurn] iter=${iter} json-tool result injected — continuing loop`);
339
+ continue;
340
+ }
341
+ }
342
+ if (decision.thought && contextWriter && chatId) {
343
+ try { await contextWriter.recordThought(chatId, decision.thought); } catch {}
344
+ }
345
+ break; // non-tool-bound response ends the loop
346
+ }
347
+ }
348
+
349
+ await echoOptInToolSummaryIfNeeded({
350
+ allEmittedEvents,
351
+ messageSender,
352
+ chatId,
353
+ context: serverContext.context,
354
+ toolManager,
355
+ });
356
+
357
+ const contextUpdates = buildContextUpdates(lastAction, allEmittedEvents);
358
+ console.log(`[runTurn] done state=${stateId} lastAction=${lastAction} events=${allEmittedEvents.length} llmCalls=${llmCallsTotal}`);
359
+
360
+ if (_services.telemetryClient) {
361
+ _services.telemetryClient.send({
362
+ source: 'agent',
363
+ chatId,
364
+ execution: {
365
+ executionLoops: llmCallsTotal,
366
+ llmCallsTotal,
367
+ tokensTotal,
368
+ tokensInput,
369
+ tokensOutput,
370
+ },
371
+ meta: { stateId, action: lastAction },
372
+ }).catch(() => {});
373
+ }
374
+
375
+ return { contextUpdates };
376
+ }
377
+
378
+ async function readServerContext({ chatId, contextReader, toolManager, messageBuffer, incomingEvent }) {
379
+ const [rawContext, tools] = await Promise.all([
380
+ contextReader.read(chatId, {}),
381
+ toolManager?.getAvailableTools?.() ?? Promise.resolve([]),
382
+ ]);
383
+
384
+ let context = rawContext;
385
+ if (!context || typeof context !== 'object') {
386
+ context = {
387
+ [ContextProperties.HISTORY]: [],
388
+ [ContextProperties.ACTIVE_TASKS]: [],
389
+ [ContextProperties.AGENTS]: [],
390
+ [ContextProperties.USERS]: [],
391
+ };
392
+ }
393
+ for (const key of [ContextProperties.HISTORY, ContextProperties.ACTIVE_TASKS, ContextProperties.AGENTS, ContextProperties.USERS]) {
394
+ if (!Array.isArray(context[key])) context[key] = [];
395
+ }
396
+
397
+ const agentId = context[ContextProperties.AGENTS]?.find(a => a.isYou)?.agentId || null;
398
+ if (messageBuffer) {
399
+ messageBuffer.merge(chatId, context[ContextProperties.HISTORY], agentId);
400
+ }
401
+
402
+ return { context, tools };
403
+ }
404
+
405
+ function buildDecisionPrompt({
406
+ statePrompt, actions, serverContext, incomingEvent, definition, promptBuilder, chatId,
407
+ stateId, machineContext,
408
+ }) {
409
+ if (typeof promptBuilder?.buildFromState !== 'function') {
410
+ throw new Error('[runTurn] promptBuilder.buildFromState is required');
411
+ }
412
+ return promptBuilder.buildFromState({
413
+ statePrompt, actions, serverContext, incomingEvent, definition, chatId,
414
+ stateId, machineContext,
415
+ });
416
+ }
417
+
418
+ function applyActionToolBinding(decision, actionDef) {
419
+ if (!resolveActionTool(actionDef, decision.action)) return;
420
+ if (decision.args == null && decision.toolArgs != null) decision.args = decision.toolArgs;
421
+ }
422
+
423
+ function parseLLMDecision(llmResponse, allowedActionNames, actions = {}) {
424
+ if (llmResponse?.tool_calls?.length) {
425
+ const n = llmResponse.tool_calls.length;
426
+ if (n > 1) {
427
+ // Multiple tool_calls per turn are not supported. Only the first is executed;
428
+ // the rest are silently dropped, which means parts of the user's request will be lost.
429
+ // If this error appears, fix the state prompt to instruct the LLM to consolidate
430
+ // all tasks into a single tool call instead of returning multiple.
431
+ console.error(`[runTurn] LLM returned ${n} tool_calls but only the first will be executed — the remaining ${n - 1} are dropped. Fix the prompt to prevent this.`);
432
+ }
433
+ const tc = llmResponse.tool_calls[0];
434
+ if (!tc?.function?.name) return null;
435
+ let args = {};
436
+ try { args = JSON.parse(tc.function.arguments || '{}'); } catch {}
437
+
438
+ const matchingAction = findActionForTool(tc.function.name, allowedActionNames, actions);
439
+ if (!matchingAction) {
440
+ console.log(`[runTurn] tool_calls: no action binds tool "${tc.function.name}"`);
441
+ return null;
442
+ }
443
+ return {
444
+ thought: `Using tool: ${tc.function.name}`,
445
+ action: matchingAction,
446
+ tool: tc.function.name,
447
+ args,
448
+ };
449
+ }
450
+
451
+ const parsed = safeParseJSON(llmResponse?.content, null);
452
+ if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
453
+ return null;
454
+ }
455
+
456
+ if (!parsed.action || !allowedActionNames.includes(parsed.action)) {
457
+ console.log('[runTurn] missing or unknown action in JSON response');
458
+ return null;
459
+ }
460
+
461
+ return parsed;
462
+ }
463
+
464
+ function findActionForTool(toolName, allowedActionNames, actions) {
465
+ if (!toolName) return null;
466
+ const byBinding = allowedActionNames.filter(
467
+ n => resolveActionTool(actions[n], n) === toolName
468
+ );
469
+ if (byBinding.length === 1) return byBinding[0];
470
+ if (byBinding.length > 1) {
471
+ console.log(`[runTurn] ambiguous: ${byBinding.length} actions bind tool "${toolName}"`);
472
+ return null;
473
+ }
474
+ return null;
475
+ }
476
+
477
+ async function executeDecision(decision, actionDef, services) {
478
+ const { messageSender, toolManager, contextWriter, taskService, operatorKey, agentId, chatId, agents, turnScratch } = services;
479
+
480
+ const boundTool = actionDef ? resolveActionTool(actionDef, decision.action) : null;
481
+
482
+ if (boundTool) {
483
+ decision.tool = boundTool;
484
+ return await executeTool(decision, toolManager, contextWriter, operatorKey, agentId, chatId, taskService, agents, turnScratch);
485
+ }
486
+
487
+ if (decision.action === '_genericDiscoveryTool' && decision.tool) {
488
+ return await executeTool(decision, toolManager, contextWriter, operatorKey, agentId, chatId, taskService, agents, turnScratch);
489
+ }
490
+
491
+ if (decision.tool) {
492
+ return await executeTool(decision, toolManager, contextWriter, operatorKey, agentId, chatId, taskService, agents, turnScratch);
493
+ }
494
+
495
+ if (decision.messages && Array.isArray(decision.messages) && decision.messages.length > 0) {
496
+ return await executeSendMessages(decision, messageSender, chatId);
497
+ }
498
+
499
+ if (decision.message && decision.receiverId) {
500
+ return await executeSendMessage(decision, messageSender, chatId);
501
+ }
502
+
503
+ if (decision.action === 'wait') {
504
+ return { type: 'waited' };
505
+ }
506
+
507
+ return null;
508
+ }
509
+
510
+ const MAX_MSG_CHARS = 3500;
511
+
512
+ function truncateMsg(msg) {
513
+ if (typeof msg !== 'string') return msg;
514
+ return msg.length > MAX_MSG_CHARS ? msg.slice(0, MAX_MSG_CHARS) + '\n\n[truncated]' : msg;
515
+ }
516
+
517
+ async function executeSendMessage(decision, messageSender, chatId) {
518
+ const { message, receiverId } = decision;
519
+ if (!message || !receiverId) return null;
520
+
521
+ const targetChatId = decision.chatId || chatId;
522
+ const safeMessage = truncateMsg(message);
523
+
524
+ if (messageSender) {
525
+ await messageSender(safeMessage, receiverId, targetChatId);
526
+ }
527
+
528
+ const events = [{ type: 'message_sent', receiverId, chatId: targetChatId, message: safeMessage, senderId: 'system' }];
529
+ if (targetChatId !== chatId) {
530
+ events.push({ type: 'report_sent', message: 'Reported to owner.', senderId: 'system' });
531
+ }
532
+ return events;
533
+ }
534
+
535
+ async function executeSendMessages(decision, messageSender, chatId) {
536
+ const events = [];
537
+ for (const msg of decision.messages) {
538
+ if (!msg.receiverId || !msg.message) continue;
539
+ const targetChatId = msg.chatId || chatId;
540
+ const safeMessage = truncateMsg(msg.message);
541
+ if (messageSender) {
542
+ try { await messageSender(safeMessage, msg.receiverId, targetChatId); } catch (e) {
543
+ console.log('[runTurn] send failed:', e.message);
544
+ continue;
545
+ }
546
+ }
547
+ events.push({ type: 'message_sent', receiverId: msg.receiverId, chatId: targetChatId, message: safeMessage, senderId: 'system' });
548
+ if (targetChatId !== chatId) {
549
+ events.push({ type: 'report_sent', message: 'Reported to owner.', senderId: 'system' });
550
+ }
551
+ }
552
+ return events;
553
+ }
554
+
555
+ async function executeTool(decision, toolManager, contextWriter, operatorKey, agentId, chatId, taskService, agents, turnScratch) {
556
+ const { tool, args } = decision;
557
+ if (!tool) return null;
558
+
559
+ const operationId = generateId();
560
+ try {
561
+ await contextWriter?.recordOperation(chatId, { operationId, type: 'tool', tool, args: args || {}, state: 'started' });
562
+ } catch {}
563
+
564
+ try {
565
+ const result = await toolManager.executeTool(tool, args || {}, {
566
+ operatorKey, agentId, chatId, taskService, agents, turnScratch,
567
+ });
568
+ try {
569
+ await contextWriter?.recordOperation(chatId, { operationId, type: 'tool', tool, args: args || {}, result, state: 'completed' });
570
+ } catch {}
571
+ return { type: 'tool_result', operationId, tool, result, senderId: 'system' };
572
+ } catch (error) {
573
+ try {
574
+ await contextWriter?.recordOperation(chatId, { operationId, type: 'tool', tool, args: args || {}, error: error.message, state: 'failed' });
575
+ } catch {}
576
+ return { type: 'tool_error', operationId, tool, error: error.message, senderId: 'system' };
577
+ }
578
+ }