@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,87 @@
|
|
|
1
|
+
import { defineTool } from '../tools/defineTool.js';
|
|
2
|
+
import { executeTaskPayload } from './taskProtocolRunner.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Built-in task protocol tools — handlers call TaskService via `executeTaskPayload`.
|
|
6
|
+
* Registered like any other tool; runTurn runs them through ToolManager and emits
|
|
7
|
+
* `tool_result`. Context updates come from `buildContextUpdates` when `tool` is a task protocol name.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
function handlerFor(operation) {
|
|
11
|
+
return async (args, ctx) => {
|
|
12
|
+
const taskService = ctx.taskService;
|
|
13
|
+
if (!taskService) {
|
|
14
|
+
throw new Error('taskService missing from tool context (expected when running under Agent)');
|
|
15
|
+
}
|
|
16
|
+
const chatId = ctx.chatId;
|
|
17
|
+
const agents = ctx.agents || [];
|
|
18
|
+
const task = { operation, ...args };
|
|
19
|
+
const result = await executeTaskPayload(taskService, task, chatId, agents, ctx);
|
|
20
|
+
if (result == null) throw new Error(`Task operation ${operation} returned no result`);
|
|
21
|
+
return result;
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export const taskMakeTaskTool = defineTool(
|
|
26
|
+
'task_make_task',
|
|
27
|
+
{
|
|
28
|
+
description: { type: 'string', required: true },
|
|
29
|
+
proposedTo: 'string',
|
|
30
|
+
executorId: 'string',
|
|
31
|
+
parentTaskId: 'string',
|
|
32
|
+
payerId: 'string',
|
|
33
|
+
contract: { type: 'object' },
|
|
34
|
+
perspective: { type: 'object' },
|
|
35
|
+
},
|
|
36
|
+
handlerFor('make-task')
|
|
37
|
+
);
|
|
38
|
+
|
|
39
|
+
export const taskMakeSubTasksTool = defineTool(
|
|
40
|
+
'task_make_sub_tasks',
|
|
41
|
+
{
|
|
42
|
+
description: { type: 'string', required: true },
|
|
43
|
+
subtasks: ['string'],
|
|
44
|
+
contract: { type: 'object' },
|
|
45
|
+
perspective: { type: 'object' },
|
|
46
|
+
parentTaskId: 'string',
|
|
47
|
+
},
|
|
48
|
+
handlerFor('make-sub-tasks')
|
|
49
|
+
);
|
|
50
|
+
|
|
51
|
+
export const taskUpdateTaskTool = defineTool(
|
|
52
|
+
'task_update_task',
|
|
53
|
+
{
|
|
54
|
+
taskId: { type: 'string', required: true },
|
|
55
|
+
status: { type: 'string', required: true, enum: ['completed', 'failed', 'cancelled'] },
|
|
56
|
+
result: 'string',
|
|
57
|
+
},
|
|
58
|
+
handlerFor('update-task')
|
|
59
|
+
);
|
|
60
|
+
|
|
61
|
+
export const taskRespondProposalTool = defineTool(
|
|
62
|
+
'task_respond_proposal',
|
|
63
|
+
{
|
|
64
|
+
taskId: { type: 'string', required: true },
|
|
65
|
+
action: { type: 'string', required: true },
|
|
66
|
+
},
|
|
67
|
+
handlerFor('respond-proposal')
|
|
68
|
+
);
|
|
69
|
+
|
|
70
|
+
export const taskUpdatePlanStepTool = defineTool(
|
|
71
|
+
'task_update_plan_step',
|
|
72
|
+
{
|
|
73
|
+
taskId: { type: 'string', required: true },
|
|
74
|
+
stepId: { type: 'string', required: true },
|
|
75
|
+
status: { type: 'string', required: true },
|
|
76
|
+
result: 'string',
|
|
77
|
+
},
|
|
78
|
+
handlerFor('update-plan-step')
|
|
79
|
+
);
|
|
80
|
+
|
|
81
|
+
export const TASK_PROTOCOL_TOOLS = [
|
|
82
|
+
taskMakeTaskTool,
|
|
83
|
+
taskMakeSubTasksTool,
|
|
84
|
+
taskUpdateTaskTool,
|
|
85
|
+
taskRespondProposalTool,
|
|
86
|
+
taskUpdatePlanStepTool,
|
|
87
|
+
];
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import Ajv from 'ajv';
|
|
2
|
+
import { ToolProvider } from './ToolProvider.js';
|
|
3
|
+
|
|
4
|
+
const ajv = new Ajv({ allErrors: true, strict: false });
|
|
5
|
+
|
|
6
|
+
export class ToolManager extends ToolProvider {
|
|
7
|
+
constructor() {
|
|
8
|
+
super();
|
|
9
|
+
this.tools = new Map();
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
register(tool) {
|
|
13
|
+
if (!tool?.schema) {
|
|
14
|
+
throw new Error('Tool must have a schema');
|
|
15
|
+
}
|
|
16
|
+
if (typeof tool.handler !== 'function') {
|
|
17
|
+
throw new Error('Tool must have a handler function');
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const name = tool.schema.function?.name || tool.schema.name;
|
|
21
|
+
if (!name) {
|
|
22
|
+
throw new Error('Tool schema must have a name');
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
this.tools.set(name, tool);
|
|
26
|
+
return name;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
registerAll(tools = []) {
|
|
30
|
+
for (const tool of tools) {
|
|
31
|
+
this.register(tool);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
async getAvailableTools() {
|
|
36
|
+
return Array.from(this.tools.values());
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
getTool(name) {
|
|
40
|
+
const normalized = this._normalizeName(name);
|
|
41
|
+
return this.tools.get(normalized) || null;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
async executeTool(name, args = {}, context = {}) {
|
|
45
|
+
const normalized = this._normalizeName(name);
|
|
46
|
+
const tool = this.tools.get(normalized);
|
|
47
|
+
|
|
48
|
+
if (!tool) {
|
|
49
|
+
const err = new Error(`Tool not found: ${name}`);
|
|
50
|
+
err.type = 'tool_not_found';
|
|
51
|
+
throw err;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
this._validateArgs(tool, args);
|
|
55
|
+
|
|
56
|
+
try {
|
|
57
|
+
return await tool.handler(args, context);
|
|
58
|
+
} catch (error) {
|
|
59
|
+
const err = new Error(`Tool "${name}" failed: ${error.message}`);
|
|
60
|
+
err.type = 'tool_error';
|
|
61
|
+
err.cause = error;
|
|
62
|
+
throw err;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
_validateArgs(tool, args) {
|
|
67
|
+
const parameters = tool.schema.function?.parameters || tool.schema.parameters;
|
|
68
|
+
if (!parameters) return;
|
|
69
|
+
|
|
70
|
+
const validate = ajv.compile(parameters);
|
|
71
|
+
if (!validate(args)) {
|
|
72
|
+
const name = tool.schema.function?.name || tool.schema.name;
|
|
73
|
+
const err = new Error(`Invalid args for "${name}": ${ajv.errorsText(validate.errors)}`);
|
|
74
|
+
err.type = 'validation_error';
|
|
75
|
+
err.data = { errors: validate.errors };
|
|
76
|
+
throw err;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Base class for tool providers.
|
|
3
|
+
* ToolManager and any custom providers extend this.
|
|
4
|
+
*/
|
|
5
|
+
export class ToolProvider {
|
|
6
|
+
async getAvailableTools() {
|
|
7
|
+
throw new Error('getAvailableTools must be implemented');
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
async executeTool(name, args = {}, context = {}) {
|
|
11
|
+
throw new Error('executeTool must be implemented');
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
getTool(name) {
|
|
15
|
+
throw new Error('getTool must be implemented');
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
_normalizeName(name) {
|
|
19
|
+
if (!name || typeof name !== 'string') return name;
|
|
20
|
+
|
|
21
|
+
const prefixes = ['functions.', 'tools.', 'function.', 'tool.'];
|
|
22
|
+
for (const prefix of prefixes) {
|
|
23
|
+
if (name.startsWith(prefix)) {
|
|
24
|
+
return name.substring(prefix.length);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
return name;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
const PRIMITIVES = new Set(['string', 'number', 'boolean', 'integer']);
|
|
2
|
+
|
|
3
|
+
function convertField(value) {
|
|
4
|
+
if (typeof value === 'string') {
|
|
5
|
+
if (!PRIMITIVES.has(value)) throw new Error(`Unknown type shorthand: "${value}"`);
|
|
6
|
+
return { schema: { type: value }, required: false };
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
if (Array.isArray(value)) {
|
|
10
|
+
if (value.length !== 1) throw new Error('Array shorthand must have exactly one element describing the item type');
|
|
11
|
+
const inner = value[0];
|
|
12
|
+
if (typeof inner === 'string') {
|
|
13
|
+
return { schema: { type: 'array', items: { type: inner } }, required: false };
|
|
14
|
+
}
|
|
15
|
+
const obj = convertObject(inner);
|
|
16
|
+
return { schema: { type: 'array', items: obj }, required: false };
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
if (typeof value === 'object' && value !== null) {
|
|
20
|
+
if (value.type && PRIMITIVES.has(value.type)) {
|
|
21
|
+
const { required: isReq, ...rest } = value;
|
|
22
|
+
return { schema: rest, required: !!isReq };
|
|
23
|
+
}
|
|
24
|
+
if (value.type === 'array') {
|
|
25
|
+
const { required: isReq, ...rest } = value;
|
|
26
|
+
return { schema: rest, required: !!isReq };
|
|
27
|
+
}
|
|
28
|
+
if (value.type === 'object') {
|
|
29
|
+
const { required: isReq, ...rest } = value;
|
|
30
|
+
return { schema: rest, required: !!isReq };
|
|
31
|
+
}
|
|
32
|
+
const obj = convertObject(value);
|
|
33
|
+
return { schema: obj, required: false };
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
throw new Error(`Cannot convert field value: ${JSON.stringify(value)}`);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function convertObject(fields) {
|
|
40
|
+
const properties = {};
|
|
41
|
+
const required = [];
|
|
42
|
+
|
|
43
|
+
for (const [key, value] of Object.entries(fields)) {
|
|
44
|
+
if (key === 'required') continue;
|
|
45
|
+
const { schema, required: isReq } = convertField(value);
|
|
46
|
+
properties[key] = schema;
|
|
47
|
+
if (isReq) required.push(key);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const result = { type: 'object', properties };
|
|
51
|
+
if (required.length > 0) result.required = required;
|
|
52
|
+
return result;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Concise tool definition that produces the { schema, handler } format
|
|
57
|
+
* expected by ToolManager.register().
|
|
58
|
+
*
|
|
59
|
+
* defineTool('my_tool', {
|
|
60
|
+
* query: { type: 'string', required: true },
|
|
61
|
+
* limit: 'number',
|
|
62
|
+
* tags: ['string'],
|
|
63
|
+
* nested: [{ name: { type: 'string', required: true }, score: 'number' }],
|
|
64
|
+
* }, async (args, ctx) => { ... })
|
|
65
|
+
*
|
|
66
|
+
* @param {object} [options]
|
|
67
|
+
* @param {boolean} [options.echoUserSummaryOnSuccess] — When true, runTurn sends `result.message`
|
|
68
|
+
* to the primary user if the model did not already send chat this turn (opt-in per tool).
|
|
69
|
+
*/
|
|
70
|
+
export function defineTool(name, params, handler, options = {}) {
|
|
71
|
+
if (!name || typeof name !== 'string') throw new Error('defineTool: name is required');
|
|
72
|
+
if (!handler || typeof handler !== 'function') throw new Error('defineTool: handler function is required');
|
|
73
|
+
|
|
74
|
+
const parameters = convertObject(params || {});
|
|
75
|
+
const echoUserSummaryOnSuccess = options.echoUserSummaryOnSuccess === true;
|
|
76
|
+
|
|
77
|
+
return {
|
|
78
|
+
schema: { type: 'function', function: { name, parameters } },
|
|
79
|
+
handler,
|
|
80
|
+
...(echoUserSummaryOnSuccess ? { echoUserSummaryOnSuccess: true } : {}),
|
|
81
|
+
};
|
|
82
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export { ToolManager } from './ToolManager.js';
|
|
2
|
+
export { ToolProvider } from './ToolProvider.js';
|
|
3
|
+
export { defineTool } from './defineTool.js';
|
|
4
|
+
export { TASK_PROTOCOL_TOOLS } from '../tasks/taskProtocolTools.js';
|
|
5
|
+
export {
|
|
6
|
+
TASK_PROTOCOL_TOOL_NAMES,
|
|
7
|
+
TASK_PROTOCOL_TOOL_TO_OPERATION,
|
|
8
|
+
mapTaskProtocolToolToOperation,
|
|
9
|
+
isTaskProtocolToolName,
|
|
10
|
+
} from '../tasks/taskProtocolRegistry.js';
|
|
11
|
+
export { executeTaskPayload } from '../tasks/taskProtocolRunner.js';
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
export function extractJSON(content) {
|
|
2
|
+
if (!content || typeof content !== 'string') return '';
|
|
3
|
+
|
|
4
|
+
const jsonBlockMatch = content.match(/```(?:json)?\s*([\s\S]*?)\s*```/);
|
|
5
|
+
if (jsonBlockMatch) {
|
|
6
|
+
content = jsonBlockMatch[1].trim();
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
const firstBrace = content.indexOf('{');
|
|
10
|
+
const firstBracket = content.indexOf('[');
|
|
11
|
+
|
|
12
|
+
let startIdx = -1;
|
|
13
|
+
let isArray = false;
|
|
14
|
+
|
|
15
|
+
if (firstBrace !== -1 && (firstBracket === -1 || firstBrace < firstBracket)) {
|
|
16
|
+
startIdx = firstBrace;
|
|
17
|
+
isArray = false;
|
|
18
|
+
} else if (firstBracket !== -1) {
|
|
19
|
+
startIdx = firstBracket;
|
|
20
|
+
isArray = true;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
if (startIdx === -1) {
|
|
24
|
+
return content.trim();
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const openChar = isArray ? '[' : '{';
|
|
28
|
+
const closeChar = isArray ? ']' : '}';
|
|
29
|
+
let depth = 0;
|
|
30
|
+
let endIdx = startIdx;
|
|
31
|
+
|
|
32
|
+
for (let i = startIdx; i < content.length; i++) {
|
|
33
|
+
const char = content[i];
|
|
34
|
+
if (char === openChar) {
|
|
35
|
+
depth++;
|
|
36
|
+
} else if (char === closeChar) {
|
|
37
|
+
depth--;
|
|
38
|
+
if (depth === 0) {
|
|
39
|
+
endIdx = i + 1;
|
|
40
|
+
break;
|
|
41
|
+
}
|
|
42
|
+
} else if (char === '"') {
|
|
43
|
+
|
|
44
|
+
i++;
|
|
45
|
+
while (i < content.length && content[i] !== '"') {
|
|
46
|
+
if (content[i] === '\\') i++;
|
|
47
|
+
i++;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (endIdx > startIdx) {
|
|
53
|
+
content = content.substring(startIdx, endIdx);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
content = content.trim();
|
|
57
|
+
|
|
58
|
+
// Don't match // inside URLs (e.g. https://...); use (?<!:) to skip ://
|
|
59
|
+
content = content.replace(/(?<!:)\/\/.*$/gm, '');
|
|
60
|
+
content = content.replace(/\/\*[\s\S]*?\*\//g, '');
|
|
61
|
+
|
|
62
|
+
content = content.replace(/,(\s*[}\]])/g, '$1');
|
|
63
|
+
|
|
64
|
+
return content;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function repairJsonStrings(jsonText) {
|
|
68
|
+
if (!jsonText || typeof jsonText !== 'string') return jsonText;
|
|
69
|
+
|
|
70
|
+
// Valid JSON escape characters after backslash
|
|
71
|
+
const validEscapes = new Set(['"', '\\', '/', 'b', 'f', 'n', 'r', 't', 'u']);
|
|
72
|
+
|
|
73
|
+
let out = '';
|
|
74
|
+
let inString = false;
|
|
75
|
+
|
|
76
|
+
for (let i = 0; i < jsonText.length; i++) {
|
|
77
|
+
const ch = jsonText[i];
|
|
78
|
+
|
|
79
|
+
if (!inString) {
|
|
80
|
+
if (ch === '"') {
|
|
81
|
+
inString = true;
|
|
82
|
+
}
|
|
83
|
+
out += ch;
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Inside a string
|
|
88
|
+
if (ch === '\\') {
|
|
89
|
+
const next = jsonText[i + 1];
|
|
90
|
+
if (next && validEscapes.has(next)) {
|
|
91
|
+
// Valid escape sequence - keep as-is
|
|
92
|
+
out += ch;
|
|
93
|
+
} else {
|
|
94
|
+
// Invalid escape (e.g., "\ " or "\x") - escape the backslash itself
|
|
95
|
+
out += '\\\\';
|
|
96
|
+
}
|
|
97
|
+
continue;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if (ch === '"') {
|
|
101
|
+
inString = false;
|
|
102
|
+
out += ch;
|
|
103
|
+
continue;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// JSON does not allow raw newlines in strings; repair them.
|
|
107
|
+
if (ch === '\n') {
|
|
108
|
+
out += '\\n';
|
|
109
|
+
continue;
|
|
110
|
+
}
|
|
111
|
+
if (ch === '\r') {
|
|
112
|
+
out += '\\r';
|
|
113
|
+
continue;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
out += ch;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
return out;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export function safeParseJSON(content, fallback = null) {
|
|
123
|
+
try {
|
|
124
|
+
const extracted = extractJSON(content);
|
|
125
|
+
if (!extracted || extracted.trim().length === 0) {
|
|
126
|
+
throw new Error('Empty JSON content');
|
|
127
|
+
}
|
|
128
|
+
try {
|
|
129
|
+
return JSON.parse(extracted);
|
|
130
|
+
} catch (e) {
|
|
131
|
+
// Common LLM failure modes: raw newlines, invalid escape sequences
|
|
132
|
+
const repaired = repairJsonStrings(extracted);
|
|
133
|
+
return JSON.parse(repaired);
|
|
134
|
+
}
|
|
135
|
+
} catch (error) {
|
|
136
|
+
console.error(`❌ [JSON Extractor] Error parsing JSON:`, content);
|
|
137
|
+
return fallback || null;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Interprets workflow definitions: parked states (transitions only) and thinking states (prompt + actions + transitions).
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { classifyIncomingEvent } from '../context/classifyEnvelope.js';
|
|
6
|
+
import { CONTEXT_RESET } from '../context/applyEffects.js';
|
|
7
|
+
|
|
8
|
+
export class AgentMachine {
|
|
9
|
+
constructor({ definition, executionCore, services, input }) {
|
|
10
|
+
this.definition = definition;
|
|
11
|
+
this.executionCore = executionCore;
|
|
12
|
+
this.state = definition.initial;
|
|
13
|
+
this.status = 'active';
|
|
14
|
+
this._running = false;
|
|
15
|
+
this._subscribers = new Set();
|
|
16
|
+
|
|
17
|
+
this.context = {
|
|
18
|
+
chatId: input?.chatId ?? null,
|
|
19
|
+
laneKey: input?.laneKey ?? null,
|
|
20
|
+
ownAgentId: input?.ownAgentId ?? null,
|
|
21
|
+
incomingEvent: null,
|
|
22
|
+
activeTaskId: null,
|
|
23
|
+
pendingProposalId: null,
|
|
24
|
+
perspectiveId: null,
|
|
25
|
+
delegatedTaskIds: [],
|
|
26
|
+
...CONTEXT_RESET,
|
|
27
|
+
_services: services || {},
|
|
28
|
+
_definition: definition,
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
getSnapshot() {
|
|
33
|
+
return { value: this.state, context: this.context, status: this.status };
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
get isParked() {
|
|
37
|
+
if (this._running) return false;
|
|
38
|
+
const sd = this.definition.states[this.state];
|
|
39
|
+
return !(sd?.prompt && sd?.actions);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
subscribe(fn) {
|
|
43
|
+
this._subscribers.add(fn);
|
|
44
|
+
return { unsubscribe: () => this._subscribers.delete(fn) };
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
send(event) {
|
|
48
|
+
if (this.status !== 'active') return Promise.resolve();
|
|
49
|
+
this._storeEvent(event);
|
|
50
|
+
this._classifyEvent(event);
|
|
51
|
+
const target = this._evaluateTransitions();
|
|
52
|
+
if (target) return this._transition(target);
|
|
53
|
+
return Promise.resolve();
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
async _transition(target) {
|
|
57
|
+
this.state = target;
|
|
58
|
+
this._notify();
|
|
59
|
+
|
|
60
|
+
const sd = this.definition.states[this.state];
|
|
61
|
+
if (!sd?.prompt || !sd?.actions) return; // parked — done
|
|
62
|
+
|
|
63
|
+
// Thinking state: run LLM
|
|
64
|
+
this._running = true;
|
|
65
|
+
try {
|
|
66
|
+
const result = await this.executionCore({
|
|
67
|
+
stateId: this.state,
|
|
68
|
+
statePrompt: sd.prompt,
|
|
69
|
+
actions: sd.actions,
|
|
70
|
+
chatId: this.context.chatId,
|
|
71
|
+
incomingEvent: this.context.incomingEvent,
|
|
72
|
+
_services: this.context._services,
|
|
73
|
+
_definition: this.context._definition,
|
|
74
|
+
machineContext: this.context,
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
const updates = result?.contextUpdates || {};
|
|
78
|
+
Object.assign(this.context, CONTEXT_RESET, updates, extractPersistentUpdates(this.context, updates));
|
|
79
|
+
this._running = false;
|
|
80
|
+
this._notify();
|
|
81
|
+
|
|
82
|
+
const next = this._evaluateTransitions();
|
|
83
|
+
if (next) await this._transition(next);
|
|
84
|
+
} catch (error) {
|
|
85
|
+
console.error(`[workflow] state=${this.state} error:`, error.message);
|
|
86
|
+
Object.assign(this.context, CONTEXT_RESET, { lastError: error.message || String(error) });
|
|
87
|
+
this._running = false;
|
|
88
|
+
this._notify();
|
|
89
|
+
|
|
90
|
+
// Catch-all: find last unconditional transition as fallback
|
|
91
|
+
if (sd.transitions) {
|
|
92
|
+
const catchall = [...sd.transitions].reverse().find(rule => {
|
|
93
|
+
if (typeof rule === 'string') return true;
|
|
94
|
+
return !rule.when;
|
|
95
|
+
});
|
|
96
|
+
if (catchall) {
|
|
97
|
+
const fallback = typeof catchall === 'string' ? resolveTarget(catchall) : resolveTarget(catchall.to);
|
|
98
|
+
await this._transition(fallback);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
_evaluateTransitions() {
|
|
105
|
+
const sd = this.definition.states[this.state];
|
|
106
|
+
if (!sd?.transitions) return null;
|
|
107
|
+
for (const rule of sd.transitions) {
|
|
108
|
+
if (typeof rule === 'string') return resolveTarget(rule);
|
|
109
|
+
const { to, when } = rule;
|
|
110
|
+
if (!when || when(this.context)) return resolveTarget(to);
|
|
111
|
+
}
|
|
112
|
+
return null;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
_storeEvent(event) {
|
|
116
|
+
if (event.event !== undefined) this.context.incomingEvent = event.event;
|
|
117
|
+
if (event.chatId != null) this.context.chatId = event.chatId;
|
|
118
|
+
if (event.laneKey != null) this.context.laneKey = event.laneKey;
|
|
119
|
+
if (event.ownAgentId != null) this.context.ownAgentId = event.ownAgentId;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
_classifyEvent(event) {
|
|
123
|
+
const flags = classifyIncomingEvent(event.event, this.context.ownAgentId);
|
|
124
|
+
|
|
125
|
+
// Delegated-subtask awareness: if a task_result wasn't recognised as
|
|
126
|
+
// relevant by the generic check but its taskId IS one we delegated,
|
|
127
|
+
// promote it so the supervising → evaluatingResult transition fires.
|
|
128
|
+
if (!flags.subtaskResult && event.event?.type === 'task_result' && event.event?.result?.taskId) {
|
|
129
|
+
const delegated = this.context.delegatedTaskIds || [];
|
|
130
|
+
if (delegated.includes(event.event.result.taskId)) {
|
|
131
|
+
const result = event.event.result;
|
|
132
|
+
flags.subtaskResult = result;
|
|
133
|
+
if (result.state === 'failed') flags.subtaskFailed = true;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
Object.assign(this.context, CONTEXT_RESET, flags, extractPersistentFromEvent(this.context, flags));
|
|
138
|
+
applyOrchestrationChainProgress(this.context, flags);
|
|
139
|
+
// Avoid re-entering evaluatingResult on the same completed subtask after handing off to the next specialist.
|
|
140
|
+
if (this.context.orchestrationContinueChain) {
|
|
141
|
+
this.context.subtaskResult = null;
|
|
142
|
+
this.context.subtaskFailed = false;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
_notify() {
|
|
147
|
+
const snapshot = this.getSnapshot();
|
|
148
|
+
for (const fn of this._subscribers) {
|
|
149
|
+
try { fn(snapshot); } catch {}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function resolveTarget(target) {
|
|
155
|
+
if (!target || typeof target !== 'string') return target;
|
|
156
|
+
return target.startsWith('#') ? target.slice(1) : target;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function extractPersistentUpdates(ctx, updates) {
|
|
160
|
+
const p = {};
|
|
161
|
+
if (updates.proposal?.taskId) p.pendingProposalId = updates.proposal.taskId;
|
|
162
|
+
// Collect all delegated task IDs — handles both single and batch parallel delegation
|
|
163
|
+
const newIds = updates._delegatedTaskIds?.length
|
|
164
|
+
? updates._delegatedTaskIds
|
|
165
|
+
: updates.delegatedTask?.taskId ? [updates.delegatedTask.taskId] : [];
|
|
166
|
+
if (newIds.length > 0) {
|
|
167
|
+
const existing = ctx.delegatedTaskIds || [];
|
|
168
|
+
const merged = [...existing];
|
|
169
|
+
for (const id of newIds) { if (!merged.includes(id)) merged.push(id); }
|
|
170
|
+
p.delegatedTaskIds = merged;
|
|
171
|
+
p.orchestrationContinueChain = false;
|
|
172
|
+
}
|
|
173
|
+
if (updates.taskCompleted || updates.taskFailed) {
|
|
174
|
+
p.activeTaskId = null;
|
|
175
|
+
p.pendingProposalId = null;
|
|
176
|
+
p.perspectiveId = null;
|
|
177
|
+
p.orchestrationSpecialistAgentId = null;
|
|
178
|
+
p.orchestrationSpecialistAgentIds = [];
|
|
179
|
+
p.orchestrationActiveStepIndex = 0;
|
|
180
|
+
p.orchestrationContinueChain = false;
|
|
181
|
+
} else {
|
|
182
|
+
const sid = ctx.orchestrationSpecialistAgentId;
|
|
183
|
+
if (typeof sid === 'string' && sid.length > 0) {
|
|
184
|
+
p.orchestrationSpecialistAgentId = sid;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
return p;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function normalizeSpecialistIdList(contract) {
|
|
191
|
+
if (!contract || typeof contract !== 'object') return [];
|
|
192
|
+
const raw = contract.specialistAgentIds;
|
|
193
|
+
const ids = [];
|
|
194
|
+
if (Array.isArray(raw)) {
|
|
195
|
+
for (const id of raw) {
|
|
196
|
+
if (typeof id === 'string' && id.length > 0 && !ids.includes(id)) ids.push(id);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
const single = contract.specialistAgentId;
|
|
200
|
+
if (ids.length === 0 && typeof single === 'string' && single.length > 0) {
|
|
201
|
+
ids.push(single);
|
|
202
|
+
}
|
|
203
|
+
return ids;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function extractPersistentFromEvent(ctx, flags) {
|
|
207
|
+
const p = {};
|
|
208
|
+
if (flags.approval) {
|
|
209
|
+
const t = flags.taskAssignment || {};
|
|
210
|
+
const tid =
|
|
211
|
+
t.taskId ||
|
|
212
|
+
t.id ||
|
|
213
|
+
(t._id != null ? String(t._id) : null) ||
|
|
214
|
+
ctx.pendingProposalId;
|
|
215
|
+
if (tid) {
|
|
216
|
+
p.activeTaskId = tid;
|
|
217
|
+
p.pendingProposalId = null;
|
|
218
|
+
p.perspectiveId = t.perspective?.ownerChatId || null;
|
|
219
|
+
}
|
|
220
|
+
const contract = t.contract && typeof t.contract === 'object' ? t.contract : null;
|
|
221
|
+
const ids = normalizeSpecialistIdList(contract);
|
|
222
|
+
if (ids.length > 0) {
|
|
223
|
+
p.orchestrationSpecialistAgentIds = ids;
|
|
224
|
+
p.orchestrationSpecialistAgentId = ids[0];
|
|
225
|
+
p.orchestrationActiveStepIndex = 0;
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
if (flags.rejection) {
|
|
229
|
+
p.pendingProposalId = null;
|
|
230
|
+
p.orchestrationSpecialistAgentIds = [];
|
|
231
|
+
p.orchestrationActiveStepIndex = 0;
|
|
232
|
+
p.orchestrationSpecialistAgentId = null;
|
|
233
|
+
}
|
|
234
|
+
return p;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* After a delegated subtask succeeds, advance multi-specialist orchestration so the next agent can run.
|
|
239
|
+
*/
|
|
240
|
+
function applyOrchestrationChainProgress(ctx, flags) {
|
|
241
|
+
ctx.orchestrationContinueChain = false;
|
|
242
|
+
if (!flags.subtaskResult || flags.subtaskFailed) return;
|
|
243
|
+
const ids = ctx.orchestrationSpecialistAgentIds;
|
|
244
|
+
if (!Array.isArray(ids) || ids.length <= 1) return;
|
|
245
|
+
const step = typeof ctx.orchestrationActiveStepIndex === 'number' ? ctx.orchestrationActiveStepIndex : 0;
|
|
246
|
+
if (step + 1 >= ids.length) return;
|
|
247
|
+
ctx.orchestrationActiveStepIndex = step + 1;
|
|
248
|
+
ctx.orchestrationSpecialistAgentId = ids[step + 1];
|
|
249
|
+
ctx.orchestrationContinueChain = true;
|
|
250
|
+
}
|