@exaudeus/workrail 3.71.0 → 3.72.0
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/dist/console-ui/assets/{index-DyREuUoq.js → index-CTza1zb5.js} +1 -1
- package/dist/console-ui/index.html +1 -1
- package/dist/daemon/session-scope.d.ts +27 -0
- package/dist/daemon/session-scope.js +21 -0
- package/dist/daemon/turn-end/conversation-flusher.d.ts +4 -0
- package/dist/daemon/turn-end/conversation-flusher.js +8 -0
- package/dist/daemon/turn-end/detect-stuck.d.ts +2 -0
- package/dist/daemon/turn-end/detect-stuck.js +5 -0
- package/dist/daemon/turn-end/step-injector.d.ts +8 -0
- package/dist/daemon/turn-end/step-injector.js +10 -0
- package/dist/daemon/workflow-runner.d.ts +53 -2
- package/dist/daemon/workflow-runner.js +400 -306
- package/dist/manifest.json +52 -20
- package/dist/mcp/handlers/v2-advance-core/outcome-success.js +1 -5
- package/dist/mcp/handlers/v2-context-budget.d.ts +0 -5
- package/dist/mcp/handlers/v2-context-budget.js +0 -17
- package/dist/trigger/trigger-listener.js +4 -3
- package/dist/trigger/trigger-router.js +7 -7
- package/dist/v2/durable-core/schemas/export-bundle/index.d.ts +16 -16
- package/docs/history/worktrain-journal.md +1945 -0
- package/docs/ideas/backlog.md +165 -12
- package/docs/reference/worktrain-daemon-invariants.md +29 -9
- package/package.json +1 -1
- package/workflows/coding-task-workflow-agentic.json +23 -8
- package/workflows/wr.research.json +158 -0
|
@@ -59,11 +59,16 @@ exports.makeSignalCoordinatorTool = makeSignalCoordinatorTool;
|
|
|
59
59
|
exports.buildSessionRecap = buildSessionRecap;
|
|
60
60
|
exports.buildSystemPrompt = buildSystemPrompt;
|
|
61
61
|
exports.tagToStatsOutcome = tagToStatsOutcome;
|
|
62
|
+
exports.sidecardLifecycleFor = sidecardLifecycleFor;
|
|
62
63
|
exports.buildAgentClient = buildAgentClient;
|
|
63
64
|
exports.createSessionState = createSessionState;
|
|
64
65
|
exports.evaluateStuckSignals = evaluateStuckSignals;
|
|
65
66
|
exports.finalizeSession = finalizeSession;
|
|
66
67
|
exports.buildSessionContext = buildSessionContext;
|
|
68
|
+
exports.buildPreAgentSession = buildPreAgentSession;
|
|
69
|
+
exports.buildTurnEndSubscriber = buildTurnEndSubscriber;
|
|
70
|
+
exports.buildAgentCallbacks = buildAgentCallbacks;
|
|
71
|
+
exports.buildSessionResult = buildSessionResult;
|
|
67
72
|
exports.runWorkflow = runWorkflow;
|
|
68
73
|
require("reflect-metadata");
|
|
69
74
|
const fs = __importStar(require("node:fs/promises"));
|
|
@@ -82,8 +87,12 @@ const v2_token_ops_js_1 = require("../mcp/handlers/v2-token-ops.js");
|
|
|
82
87
|
const index_js_2 = require("../v2/durable-core/ids/index.js");
|
|
83
88
|
const node_outputs_js_1 = require("../v2/projections/node-outputs.js");
|
|
84
89
|
const assert_never_js_1 = require("../runtime/assert-never.js");
|
|
90
|
+
const result_js_1 = require("../runtime/result.js");
|
|
85
91
|
const session_recovery_policy_js_1 = require("./session-recovery-policy.js");
|
|
86
92
|
const stats_summary_js_1 = require("./stats-summary.js");
|
|
93
|
+
const step_injector_js_1 = require("./turn-end/step-injector.js");
|
|
94
|
+
const conversation_flusher_js_1 = require("./turn-end/conversation-flusher.js");
|
|
95
|
+
const session_scope_js_1 = require("./session-scope.js");
|
|
87
96
|
const execAsync = (0, node_util_1.promisify)(node_child_process_1.exec);
|
|
88
97
|
const execFileAsync = (0, node_util_1.promisify)(node_child_process_1.execFile);
|
|
89
98
|
const BASH_TIMEOUT_MS = 5 * 60 * 1000;
|
|
@@ -122,22 +131,29 @@ var soul_template_js_2 = require("./soul-template.js");
|
|
|
122
131
|
Object.defineProperty(exports, "DAEMON_SOUL_DEFAULT", { enumerable: true, get: function () { return soul_template_js_2.DAEMON_SOUL_DEFAULT; } });
|
|
123
132
|
Object.defineProperty(exports, "DAEMON_SOUL_TEMPLATE", { enumerable: true, get: function () { return soul_template_js_2.DAEMON_SOUL_TEMPLATE; } });
|
|
124
133
|
async function persistTokens(sessionId, continueToken, checkpointToken, worktreePath, recoveryContext) {
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
134
|
+
try {
|
|
135
|
+
await fs.mkdir(exports.DAEMON_SESSIONS_DIR, { recursive: true });
|
|
136
|
+
const sessionPath = path.join(exports.DAEMON_SESSIONS_DIR, `${sessionId}.json`);
|
|
137
|
+
const state = JSON.stringify({
|
|
138
|
+
continueToken,
|
|
139
|
+
checkpointToken,
|
|
140
|
+
ts: Date.now(),
|
|
141
|
+
...(worktreePath !== undefined ? { worktreePath } : {}),
|
|
142
|
+
...(recoveryContext !== undefined ? {
|
|
143
|
+
workflowId: recoveryContext.workflowId,
|
|
144
|
+
goal: recoveryContext.goal,
|
|
145
|
+
workspacePath: recoveryContext.workspacePath,
|
|
146
|
+
} : {}),
|
|
147
|
+
}, null, 2);
|
|
148
|
+
const tmp = `${sessionPath}.tmp`;
|
|
149
|
+
await fs.writeFile(tmp, state, 'utf8');
|
|
150
|
+
await fs.rename(tmp, sessionPath);
|
|
151
|
+
return (0, result_js_1.ok)(undefined);
|
|
152
|
+
}
|
|
153
|
+
catch (e) {
|
|
154
|
+
const nodeErr = e;
|
|
155
|
+
return (0, result_js_1.err)({ code: nodeErr.code ?? 'UNKNOWN', message: nodeErr.message ?? String(e) });
|
|
156
|
+
}
|
|
141
157
|
}
|
|
142
158
|
async function appendConversationMessages(filePath, messages) {
|
|
143
159
|
if (messages.length === 0)
|
|
@@ -199,7 +215,7 @@ async function readAllDaemonSessions(sessionsDir = exports.DAEMON_SESSIONS_DIR)
|
|
|
199
215
|
}
|
|
200
216
|
return sessions;
|
|
201
217
|
}
|
|
202
|
-
async function runStartupRecovery(sessionsDir = exports.DAEMON_SESSIONS_DIR, execFn = execFileAsync, ctx, _countStepAdvancesFn = countOrphanStepAdvances, _executeContinueWorkflowFn = index_js_1.executeContinueWorkflow, _runWorkflowFn = runWorkflow) {
|
|
218
|
+
async function runStartupRecovery(sessionsDir = exports.DAEMON_SESSIONS_DIR, execFn = execFileAsync, ctx, _countStepAdvancesFn = countOrphanStepAdvances, _executeContinueWorkflowFn = index_js_1.executeContinueWorkflow, _runWorkflowFn = runWorkflow, apiKey = '') {
|
|
203
219
|
await clearQueueIssueSidecars(sessionsDir);
|
|
204
220
|
const sessions = await readAllDaemonSessions(sessionsDir);
|
|
205
221
|
if (sessions.length === 0) {
|
|
@@ -308,7 +324,7 @@ async function runStartupRecovery(sessionsDir = exports.DAEMON_SESSIONS_DIR, exe
|
|
|
308
324
|
};
|
|
309
325
|
console.log(`[WorkflowRunner] Startup recovery: resuming session ${session.sessionId} ` +
|
|
310
326
|
`workflowId=${session.workflowId} stepAdvances=${stepAdvances}`);
|
|
311
|
-
void _runWorkflowFn(recoveredTrigger, ctx,
|
|
327
|
+
void _runWorkflowFn(recoveredTrigger, ctx, apiKey).then((result) => {
|
|
312
328
|
console.log(`[WorkflowRunner] Startup recovery: resumed session ${session.sessionId} completed: ${result._tag}`);
|
|
313
329
|
}).catch((err) => {
|
|
314
330
|
console.warn(`[WorkflowRunner] Startup recovery: resumed session ${session.sessionId} failed: ` +
|
|
@@ -728,7 +744,10 @@ function makeContinueWorkflowTool(sessionId, ctx, onAdvance, onComplete, schemas
|
|
|
728
744
|
const checkpointToken = out.checkpointToken ?? null;
|
|
729
745
|
const persistToken = (out.kind === 'blocked' ? out.nextCall?.params.continueToken : undefined) ?? continueToken;
|
|
730
746
|
if (persistToken) {
|
|
731
|
-
await persistTokens(sessionId, persistToken, checkpointToken);
|
|
747
|
+
const persistResult = await persistTokens(sessionId, persistToken, checkpointToken);
|
|
748
|
+
if (persistResult.kind === 'err') {
|
|
749
|
+
console.warn(`[WorkflowRunner] persistTokens failed (continue_workflow): ${persistResult.error.code} -- ${persistResult.error.message}`);
|
|
750
|
+
}
|
|
732
751
|
}
|
|
733
752
|
if (out.kind === 'blocked') {
|
|
734
753
|
const retryToken = out.nextCall?.params.continueToken ?? continueToken;
|
|
@@ -829,7 +848,10 @@ function makeCompleteStepTool(sessionId, ctx, getCurrentToken, onAdvance, onComp
|
|
|
829
848
|
const checkpointToken = out.checkpointToken ?? null;
|
|
830
849
|
const persistToken = (out.kind === 'blocked' ? out.nextCall?.params.continueToken : undefined) ?? newContinueToken;
|
|
831
850
|
if (persistToken) {
|
|
832
|
-
await persistTokens(sessionId, persistToken, checkpointToken);
|
|
851
|
+
const persistResult = await persistTokens(sessionId, persistToken, checkpointToken);
|
|
852
|
+
if (persistResult.kind === 'err') {
|
|
853
|
+
console.warn(`[WorkflowRunner] persistTokens failed (complete_step): ${persistResult.error.code} -- ${persistResult.error.message}`);
|
|
854
|
+
}
|
|
833
855
|
}
|
|
834
856
|
if (out.kind === 'blocked') {
|
|
835
857
|
const retryToken = out.nextCall?.params.continueToken ?? newContinueToken;
|
|
@@ -903,6 +925,8 @@ function makeBashTool(workspacePath, schemas, sessionId, emitter, workrailSessio
|
|
|
903
925
|
inputSchema: schemas['BashParams'],
|
|
904
926
|
label: 'Bash',
|
|
905
927
|
execute: async (_toolCallId, params) => {
|
|
928
|
+
if (typeof params.command !== 'string' || !params.command)
|
|
929
|
+
throw new Error('Bash: command must be a non-empty string');
|
|
906
930
|
console.log(`[WorkflowRunner] Tool: bash "${String(params.command).slice(0, 80)}"`);
|
|
907
931
|
if (sessionId)
|
|
908
932
|
emitter?.emit({ kind: 'tool_called', sessionId, toolName: 'Bash', summary: String(params.command).slice(0, 80), ...withWorkrailSession(workrailSessionId) });
|
|
@@ -964,6 +988,8 @@ function makeReadTool(readFileState, schemas, sessionId, emitter, workrailSessio
|
|
|
964
988
|
inputSchema: schemas['ReadParams'],
|
|
965
989
|
label: 'Read',
|
|
966
990
|
execute: async (_toolCallId, params) => {
|
|
991
|
+
if (typeof params.filePath !== 'string' || !params.filePath)
|
|
992
|
+
throw new Error('Read: filePath must be a non-empty string');
|
|
967
993
|
const filePath = params.filePath;
|
|
968
994
|
if (sessionId)
|
|
969
995
|
emitter?.emit({ kind: 'tool_called', sessionId, toolName: 'Read', summary: filePath.slice(0, 80), ...withWorkrailSession(workrailSessionId) });
|
|
@@ -1002,6 +1028,10 @@ function makeWriteTool(readFileState, schemas, sessionId, emitter, workrailSessi
|
|
|
1002
1028
|
inputSchema: schemas['WriteParams'],
|
|
1003
1029
|
label: 'Write',
|
|
1004
1030
|
execute: async (_toolCallId, params) => {
|
|
1031
|
+
if (typeof params.filePath !== 'string' || !params.filePath)
|
|
1032
|
+
throw new Error('Write: filePath must be a non-empty string');
|
|
1033
|
+
if (typeof params.content !== 'string')
|
|
1034
|
+
throw new Error('Write: content must be a string');
|
|
1005
1035
|
const filePath = params.filePath;
|
|
1006
1036
|
if (sessionId)
|
|
1007
1037
|
emitter?.emit({ kind: 'tool_called', sessionId, toolName: 'Write', summary: filePath.slice(0, 80), ...withWorkrailSession(workrailSessionId) });
|
|
@@ -1042,6 +1072,8 @@ function makeGlobTool(workspacePath, schemas, sessionId, emitter, workrailSessio
|
|
|
1042
1072
|
inputSchema: schemas['GlobParams'],
|
|
1043
1073
|
label: 'Glob',
|
|
1044
1074
|
execute: async (_toolCallId, params) => {
|
|
1075
|
+
if (typeof params.pattern !== 'string' || !params.pattern)
|
|
1076
|
+
throw new Error('Glob: pattern must be a non-empty string');
|
|
1045
1077
|
const pattern = params.pattern;
|
|
1046
1078
|
const searchRoot = params.path ?? workspacePath;
|
|
1047
1079
|
if (sessionId)
|
|
@@ -1091,6 +1123,8 @@ function makeGrepTool(workspacePath, schemas, sessionId, emitter, workrailSessio
|
|
|
1091
1123
|
inputSchema: schemas['GrepParams'],
|
|
1092
1124
|
label: 'Grep',
|
|
1093
1125
|
execute: async (_toolCallId, params) => {
|
|
1126
|
+
if (typeof params.pattern !== 'string' || !params.pattern)
|
|
1127
|
+
throw new Error('Grep: pattern must be a non-empty string');
|
|
1094
1128
|
const pattern = params.pattern;
|
|
1095
1129
|
const searchPath = params.path ?? workspacePath;
|
|
1096
1130
|
const outputMode = params.output_mode ?? 'files_with_matches';
|
|
@@ -1167,6 +1201,12 @@ function makeEditTool(workspacePath, readFileState, schemas, sessionId, emitter,
|
|
|
1167
1201
|
inputSchema: schemas['EditParams'],
|
|
1168
1202
|
label: 'Edit',
|
|
1169
1203
|
execute: async (_toolCallId, params) => {
|
|
1204
|
+
if (typeof params.file_path !== 'string' || !params.file_path)
|
|
1205
|
+
throw new Error('Edit: file_path must be a non-empty string');
|
|
1206
|
+
if (typeof params.old_string !== 'string')
|
|
1207
|
+
throw new Error('Edit: old_string must be a string');
|
|
1208
|
+
if (typeof params.new_string !== 'string')
|
|
1209
|
+
throw new Error('Edit: new_string must be a string');
|
|
1170
1210
|
const rawFilePath = params.file_path;
|
|
1171
1211
|
const absoluteFilePath = path.isAbsolute(rawFilePath)
|
|
1172
1212
|
? rawFilePath
|
|
@@ -1236,6 +1276,12 @@ function makeSpawnAgentTool(sessionId, ctx, apiKey, thisWorkrailSessionId, curre
|
|
|
1236
1276
|
inputSchema: schemas['SpawnAgentParams'],
|
|
1237
1277
|
label: 'Spawn Agent',
|
|
1238
1278
|
execute: async (_toolCallId, params) => {
|
|
1279
|
+
if (typeof params.workflowId !== 'string' || !params.workflowId)
|
|
1280
|
+
throw new Error('spawn_agent: workflowId must be a non-empty string');
|
|
1281
|
+
if (typeof params.goal !== 'string' || !params.goal)
|
|
1282
|
+
throw new Error('spawn_agent: goal must be a non-empty string');
|
|
1283
|
+
if (typeof params.workspacePath !== 'string' || !params.workspacePath)
|
|
1284
|
+
throw new Error('spawn_agent: workspacePath must be a non-empty string');
|
|
1239
1285
|
console.log(`[WorkflowRunner] Tool: spawn_agent sessionId=${sessionId} workflowId=${String(params.workflowId)} depth=${currentDepth}/${maxDepth}`);
|
|
1240
1286
|
emitter?.emit({ kind: 'tool_called', sessionId, toolName: 'spawn_agent', summary: `${String(params.workflowId)} depth=${currentDepth}`, ...withWorkrailSession(thisWorkrailSessionId) });
|
|
1241
1287
|
if (currentDepth >= maxDepth) {
|
|
@@ -1415,6 +1461,12 @@ function makeReportIssueTool(sessionId, emitter, workrailSessionId, issuesDirOve
|
|
|
1415
1461
|
},
|
|
1416
1462
|
label: 'report_issue',
|
|
1417
1463
|
execute: async (_toolCallId, params) => {
|
|
1464
|
+
if (typeof params.kind !== 'string' || !params.kind)
|
|
1465
|
+
throw new Error('report_issue: kind must be a non-empty string');
|
|
1466
|
+
if (typeof params.severity !== 'string' || !params.severity)
|
|
1467
|
+
throw new Error('report_issue: severity must be a non-empty string');
|
|
1468
|
+
if (typeof params.summary !== 'string' || !params.summary)
|
|
1469
|
+
throw new Error('report_issue: summary must be a non-empty string');
|
|
1418
1470
|
const record = {
|
|
1419
1471
|
sessionId,
|
|
1420
1472
|
kind: params.kind,
|
|
@@ -1486,6 +1538,8 @@ function makeSignalCoordinatorTool(sessionId, emitter, workrailSessionId, signal
|
|
|
1486
1538
|
},
|
|
1487
1539
|
label: 'signal_coordinator',
|
|
1488
1540
|
execute: async (_toolCallId, params) => {
|
|
1541
|
+
if (typeof params.signalKind !== 'string' || !params.signalKind)
|
|
1542
|
+
throw new Error('signal_coordinator: signalKind must be a non-empty string');
|
|
1489
1543
|
const signalId = 'sig_' + (0, node_crypto_1.randomUUID)().replace(/-/g, '').slice(0, 8);
|
|
1490
1544
|
const signalKind = String(params.signalKind ?? 'progress');
|
|
1491
1545
|
const payload = (typeof params.payload === 'object' && params.payload !== null && !Array.isArray(params.payload))
|
|
@@ -1629,6 +1683,22 @@ function tagToStatsOutcome(tag) {
|
|
|
1629
1683
|
default: return (0, assert_never_js_1.assertNever)(tag);
|
|
1630
1684
|
}
|
|
1631
1685
|
}
|
|
1686
|
+
function sidecardLifecycleFor(tag, branchStrategy) {
|
|
1687
|
+
switch (tag) {
|
|
1688
|
+
case 'success':
|
|
1689
|
+
return branchStrategy === 'worktree'
|
|
1690
|
+
? { kind: 'retain_for_delivery' }
|
|
1691
|
+
: { kind: 'delete_now' };
|
|
1692
|
+
case 'error':
|
|
1693
|
+
case 'timeout':
|
|
1694
|
+
case 'stuck':
|
|
1695
|
+
return { kind: 'delete_now' };
|
|
1696
|
+
case 'delivery_failed':
|
|
1697
|
+
throw new Error(`sidecardLifecycleFor: delivery_failed is not a valid input (invariant 1.2)`);
|
|
1698
|
+
default:
|
|
1699
|
+
return (0, assert_never_js_1.assertNever)(tag);
|
|
1700
|
+
}
|
|
1701
|
+
}
|
|
1632
1702
|
function buildAgentClient(trigger, apiKey, env) {
|
|
1633
1703
|
if (trigger.agentConfig?.model) {
|
|
1634
1704
|
const slashIdx = trigger.agentConfig.model.indexOf('/');
|
|
@@ -1726,9 +1796,15 @@ async function finalizeSession(result, ctx) {
|
|
|
1726
1796
|
ctx.daemonRegistry?.unregister(ctx.workrailSessionId, result._tag === 'success' || result._tag === 'delivery_failed' ? 'completed' : 'failed');
|
|
1727
1797
|
}
|
|
1728
1798
|
writeExecutionStats(ctx.statsDir, ctx.sessionId, ctx.workflowId, ctx.startMs, outcome, ctx.stepAdvanceCount);
|
|
1729
|
-
const
|
|
1730
|
-
|
|
1731
|
-
|
|
1799
|
+
const lifecycle = sidecardLifecycleFor(result._tag, ctx.branchStrategy);
|
|
1800
|
+
switch (lifecycle.kind) {
|
|
1801
|
+
case 'delete_now':
|
|
1802
|
+
await fs.unlink(path.join(ctx.sessionsDir, `${ctx.sessionId}.json`)).catch(() => { });
|
|
1803
|
+
break;
|
|
1804
|
+
case 'retain_for_delivery':
|
|
1805
|
+
break;
|
|
1806
|
+
default:
|
|
1807
|
+
(0, assert_never_js_1.assertNever)(lifecycle);
|
|
1732
1808
|
}
|
|
1733
1809
|
if (result._tag === 'success' && ctx.branchStrategy !== 'worktree') {
|
|
1734
1810
|
await fs.unlink(ctx.conversationPath).catch(() => { });
|
|
@@ -1747,18 +1823,7 @@ function buildSessionContext(trigger, inputs) {
|
|
|
1747
1823
|
const maxTurns = trigger.agentConfig?.maxTurns ?? exports.DEFAULT_MAX_TURNS;
|
|
1748
1824
|
return { systemPrompt, initialPrompt, sessionTimeoutMs, maxTurns };
|
|
1749
1825
|
}
|
|
1750
|
-
async function
|
|
1751
|
-
const statsDir = _statsDir ?? DAEMON_STATS_DIR;
|
|
1752
|
-
const sessionsDir = _sessionsDir ?? exports.DAEMON_SESSIONS_DIR;
|
|
1753
|
-
const startMs = Date.now();
|
|
1754
|
-
const sessionId = (0, node_crypto_1.randomUUID)();
|
|
1755
|
-
console.log(`[WorkflowRunner] Session started: sessionId=${sessionId} workflowId=${trigger.workflowId}`);
|
|
1756
|
-
emitter?.emit({
|
|
1757
|
-
kind: 'session_started',
|
|
1758
|
-
sessionId,
|
|
1759
|
-
workflowId: trigger.workflowId,
|
|
1760
|
-
workspacePath: trigger.workspacePath,
|
|
1761
|
-
});
|
|
1826
|
+
async function buildPreAgentSession(trigger, ctx, apiKey, sessionId, startMs, statsDir, sessionsDir, emitter, daemonRegistry, steerRegistry) {
|
|
1762
1827
|
let agentClient;
|
|
1763
1828
|
let modelId;
|
|
1764
1829
|
try {
|
|
@@ -1776,32 +1841,12 @@ async function runWorkflow(trigger, ctx, apiKey, daemonRegistry, emitter, steerR
|
|
|
1776
1841
|
}
|
|
1777
1842
|
}
|
|
1778
1843
|
}
|
|
1779
|
-
catch (
|
|
1780
|
-
const message =
|
|
1844
|
+
catch (e) {
|
|
1845
|
+
const message = e instanceof Error ? e.message : String(e);
|
|
1781
1846
|
writeExecutionStats(statsDir, sessionId, trigger.workflowId, startMs, 'error', 0);
|
|
1782
|
-
return {
|
|
1783
|
-
_tag: 'error',
|
|
1784
|
-
workflowId: trigger.workflowId,
|
|
1785
|
-
message,
|
|
1786
|
-
stopReason: 'error',
|
|
1787
|
-
};
|
|
1847
|
+
return { kind: 'complete', result: { _tag: 'error', workflowId: trigger.workflowId, message, stopReason: 'error' } };
|
|
1788
1848
|
}
|
|
1789
1849
|
const state = createSessionState('');
|
|
1790
|
-
const MAX_ISSUE_SUMMARIES = 10;
|
|
1791
|
-
const STUCK_REPEAT_THRESHOLD = 3;
|
|
1792
|
-
const onAdvance = (stepText, continueToken) => {
|
|
1793
|
-
state.pendingSteerParts.push(stepText);
|
|
1794
|
-
state.stepAdvanceCount++;
|
|
1795
|
-
state.currentContinueToken = continueToken;
|
|
1796
|
-
if (state.workrailSessionId !== null)
|
|
1797
|
-
daemonRegistry?.heartbeat(state.workrailSessionId);
|
|
1798
|
-
emitter?.emit({ kind: 'step_advanced', sessionId, ...withWorkrailSession(state.workrailSessionId) });
|
|
1799
|
-
};
|
|
1800
|
-
const onComplete = (notes, artifacts) => {
|
|
1801
|
-
state.isComplete = true;
|
|
1802
|
-
state.lastStepNotes = notes;
|
|
1803
|
-
state.lastStepArtifacts = artifacts;
|
|
1804
|
-
};
|
|
1805
1850
|
let firstStep;
|
|
1806
1851
|
if (trigger._preAllocatedStartResponse !== undefined) {
|
|
1807
1852
|
firstStep = trigger._preAllocatedStartResponse;
|
|
@@ -1811,41 +1856,47 @@ async function runWorkflow(trigger, ctx, apiKey, daemonRegistry, emitter, steerR
|
|
|
1811
1856
|
if (startResult.isErr()) {
|
|
1812
1857
|
writeExecutionStats(statsDir, sessionId, trigger.workflowId, startMs, 'error', 0);
|
|
1813
1858
|
return {
|
|
1814
|
-
|
|
1815
|
-
|
|
1816
|
-
|
|
1817
|
-
|
|
1859
|
+
kind: 'complete',
|
|
1860
|
+
result: {
|
|
1861
|
+
_tag: 'error',
|
|
1862
|
+
workflowId: trigger.workflowId,
|
|
1863
|
+
message: `start_workflow failed: ${startResult.error.kind} -- ${JSON.stringify(startResult.error)}`,
|
|
1864
|
+
stopReason: 'error',
|
|
1865
|
+
},
|
|
1818
1866
|
};
|
|
1819
1867
|
}
|
|
1820
1868
|
firstStep = startResult.value.response;
|
|
1821
1869
|
}
|
|
1822
|
-
const
|
|
1823
|
-
const
|
|
1824
|
-
state.currentContinueToken =
|
|
1825
|
-
if (
|
|
1826
|
-
const decoded = await (0, v2_token_ops_js_1.parseContinueTokenOrFail)(
|
|
1870
|
+
const continueToken = firstStep.continueToken ?? '';
|
|
1871
|
+
const checkpointToken = firstStep.checkpointToken ?? null;
|
|
1872
|
+
state.currentContinueToken = continueToken;
|
|
1873
|
+
if (continueToken) {
|
|
1874
|
+
const decoded = await (0, v2_token_ops_js_1.parseContinueTokenOrFail)(continueToken, ctx.v2.tokenCodecPorts, ctx.v2.tokenAliasStore);
|
|
1827
1875
|
if (decoded.isOk()) {
|
|
1828
1876
|
state.workrailSessionId = decoded.value.sessionId;
|
|
1829
1877
|
}
|
|
1830
1878
|
else {
|
|
1831
|
-
console.error(`[WorkflowRunner] Error: could not decode WorkRail session ID from continueToken -- isLive and liveActivity will not work
|
|
1879
|
+
console.error(`[WorkflowRunner] Error: could not decode WorkRail session ID from continueToken -- isLive and liveActivity will not work. Reason: ${decoded.error.message}`);
|
|
1832
1880
|
}
|
|
1833
1881
|
}
|
|
1834
|
-
if (
|
|
1835
|
-
|
|
1836
|
-
}
|
|
1837
|
-
if (state.workrailSessionId !== null) {
|
|
1838
|
-
steerRegistry?.set(state.workrailSessionId, (text) => { state.pendingSteerParts.push(text); });
|
|
1839
|
-
}
|
|
1840
|
-
if (state.workrailSessionId !== null) {
|
|
1841
|
-
abortRegistry?.set(state.workrailSessionId, () => { agent.abort(); });
|
|
1842
|
-
}
|
|
1843
|
-
if (startContinueToken) {
|
|
1844
|
-
await persistTokens(sessionId, startContinueToken, startCheckpointToken, undefined, {
|
|
1882
|
+
if (continueToken) {
|
|
1883
|
+
const persistResult = await persistTokens(sessionId, continueToken, checkpointToken, undefined, {
|
|
1845
1884
|
workflowId: trigger.workflowId,
|
|
1846
1885
|
goal: trigger.goal,
|
|
1847
1886
|
workspacePath: trigger.workspacePath,
|
|
1848
1887
|
});
|
|
1888
|
+
if (persistResult.kind === 'err') {
|
|
1889
|
+
writeExecutionStats(statsDir, sessionId, trigger.workflowId, startMs, 'error', 0);
|
|
1890
|
+
return {
|
|
1891
|
+
kind: 'complete',
|
|
1892
|
+
result: {
|
|
1893
|
+
_tag: 'error',
|
|
1894
|
+
workflowId: trigger.workflowId,
|
|
1895
|
+
message: `Initial token persist failed: ${persistResult.error.code} -- ${persistResult.error.message}`,
|
|
1896
|
+
stopReason: 'error',
|
|
1897
|
+
},
|
|
1898
|
+
};
|
|
1899
|
+
}
|
|
1849
1900
|
}
|
|
1850
1901
|
let sessionWorkspacePath = trigger.workspacePath;
|
|
1851
1902
|
let sessionWorktreePath;
|
|
@@ -1864,113 +1915,170 @@ async function runWorkflow(trigger, ctx, apiKey, daemonRegistry, emitter, steerR
|
|
|
1864
1915
|
'-b', `${branchPrefix}${sessionId}`,
|
|
1865
1916
|
`origin/${baseBranch}`,
|
|
1866
1917
|
]);
|
|
1867
|
-
await persistTokens(sessionId,
|
|
1868
|
-
|
|
1869
|
-
|
|
1870
|
-
|
|
1871
|
-
|
|
1872
|
-
|
|
1873
|
-
|
|
1918
|
+
const worktreePersistResult = await persistTokens(sessionId, continueToken ?? state.currentContinueToken, checkpointToken, sessionWorktreePath, { workflowId: trigger.workflowId, goal: trigger.goal, workspacePath: trigger.workspacePath });
|
|
1919
|
+
if (worktreePersistResult.kind === 'err') {
|
|
1920
|
+
console.error(`[WorkflowRunner] Worktree sidecar persist failed: ${worktreePersistResult.error.code} -- ${worktreePersistResult.error.message}`);
|
|
1921
|
+
try {
|
|
1922
|
+
await execFileAsync('git', ['-C', trigger.workspacePath, 'worktree', 'remove', '--force', sessionWorkspacePath]);
|
|
1923
|
+
}
|
|
1924
|
+
catch { }
|
|
1925
|
+
writeExecutionStats(statsDir, sessionId, trigger.workflowId, startMs, 'error', 0);
|
|
1926
|
+
return {
|
|
1927
|
+
kind: 'complete',
|
|
1928
|
+
result: {
|
|
1929
|
+
_tag: 'error',
|
|
1930
|
+
workflowId: trigger.workflowId,
|
|
1931
|
+
message: `Worktree sidecar persist failed: ${worktreePersistResult.error.code} -- ${worktreePersistResult.error.message}`,
|
|
1932
|
+
stopReason: 'error',
|
|
1933
|
+
},
|
|
1934
|
+
};
|
|
1935
|
+
}
|
|
1936
|
+
console.log(`[WorkflowRunner] Worktree created: sessionId=${sessionId} branch=${branchPrefix}${sessionId} path=${sessionWorkspacePath}`);
|
|
1874
1937
|
}
|
|
1875
|
-
catch (
|
|
1876
|
-
const errMsg =
|
|
1938
|
+
catch (e) {
|
|
1939
|
+
const errMsg = e instanceof Error ? e.message : String(e);
|
|
1877
1940
|
console.error(`[WorkflowRunner] Worktree creation failed: sessionId=${sessionId} error=${errMsg}`);
|
|
1878
|
-
emitter?.emit({ kind: 'session_completed', sessionId, workflowId: trigger.workflowId, outcome: 'error', detail: errMsg.slice(0, 200), ...withWorkrailSession(state.workrailSessionId) });
|
|
1879
|
-
if (state.workrailSessionId !== null)
|
|
1880
|
-
daemonRegistry?.unregister(state.workrailSessionId, 'failed');
|
|
1881
|
-
if (state.workrailSessionId !== null) {
|
|
1882
|
-
steerRegistry?.delete(state.workrailSessionId);
|
|
1883
|
-
abortRegistry?.delete(state.workrailSessionId);
|
|
1884
|
-
}
|
|
1885
1941
|
writeExecutionStats(statsDir, sessionId, trigger.workflowId, startMs, 'error', 0);
|
|
1886
1942
|
return {
|
|
1887
|
-
|
|
1888
|
-
workflowId: trigger.workflowId,
|
|
1889
|
-
message: `Worktree creation failed: ${errMsg}`,
|
|
1890
|
-
stopReason: 'error',
|
|
1943
|
+
kind: 'complete',
|
|
1944
|
+
result: { _tag: 'error', workflowId: trigger.workflowId, message: `Worktree creation failed: ${errMsg}`, stopReason: 'error' },
|
|
1891
1945
|
};
|
|
1892
1946
|
}
|
|
1893
1947
|
}
|
|
1948
|
+
if (state.workrailSessionId !== null) {
|
|
1949
|
+
daemonRegistry?.register(state.workrailSessionId, trigger.workflowId);
|
|
1950
|
+
steerRegistry?.set(state.workrailSessionId, (text) => { state.pendingSteerParts.push(text); });
|
|
1951
|
+
}
|
|
1894
1952
|
if (firstStep.isComplete) {
|
|
1895
|
-
|
|
1953
|
+
const lifecycle = sidecardLifecycleFor('success', trigger.branchStrategy);
|
|
1954
|
+
if (lifecycle.kind === 'delete_now') {
|
|
1896
1955
|
await fs.unlink(path.join(sessionsDir, `${sessionId}.json`)).catch(() => { });
|
|
1897
1956
|
}
|
|
1898
1957
|
emitter?.emit({ kind: 'session_completed', sessionId, workflowId: trigger.workflowId, outcome: 'success', detail: 'stop', ...withWorkrailSession(state.workrailSessionId) });
|
|
1899
|
-
if (state.workrailSessionId !== null)
|
|
1900
|
-
daemonRegistry?.unregister(state.workrailSessionId, 'completed');
|
|
1901
1958
|
if (state.workrailSessionId !== null) {
|
|
1959
|
+
daemonRegistry?.unregister(state.workrailSessionId, 'completed');
|
|
1902
1960
|
steerRegistry?.delete(state.workrailSessionId);
|
|
1903
|
-
abortRegistry?.delete(state.workrailSessionId);
|
|
1904
1961
|
}
|
|
1905
1962
|
writeExecutionStats(statsDir, sessionId, trigger.workflowId, startMs, 'success', 0);
|
|
1906
1963
|
return {
|
|
1907
|
-
|
|
1908
|
-
|
|
1909
|
-
|
|
1910
|
-
|
|
1911
|
-
|
|
1912
|
-
|
|
1964
|
+
kind: 'complete',
|
|
1965
|
+
result: {
|
|
1966
|
+
_tag: 'success',
|
|
1967
|
+
workflowId: trigger.workflowId,
|
|
1968
|
+
stopReason: 'stop',
|
|
1969
|
+
...(sessionWorktreePath !== undefined ? { sessionWorkspacePath: sessionWorktreePath } : {}),
|
|
1970
|
+
...(sessionWorktreePath !== undefined ? { sessionId } : {}),
|
|
1971
|
+
...(trigger.botIdentity !== undefined ? { botIdentity: trigger.botIdentity } : {}),
|
|
1972
|
+
},
|
|
1913
1973
|
};
|
|
1914
1974
|
}
|
|
1915
|
-
|
|
1916
|
-
|
|
1917
|
-
|
|
1918
|
-
|
|
1919
|
-
|
|
1920
|
-
|
|
1921
|
-
|
|
1922
|
-
|
|
1923
|
-
|
|
1924
|
-
|
|
1925
|
-
|
|
1926
|
-
|
|
1927
|
-
|
|
1928
|
-
|
|
1929
|
-
|
|
1975
|
+
return {
|
|
1976
|
+
kind: 'ready',
|
|
1977
|
+
session: {
|
|
1978
|
+
sessionId,
|
|
1979
|
+
workrailSessionId: state.workrailSessionId,
|
|
1980
|
+
continueToken,
|
|
1981
|
+
checkpointToken,
|
|
1982
|
+
sessionWorkspacePath,
|
|
1983
|
+
sessionWorktreePath,
|
|
1984
|
+
firstStep,
|
|
1985
|
+
state,
|
|
1986
|
+
spawnCurrentDepth: trigger.spawnDepth ?? 0,
|
|
1987
|
+
spawnMaxDepth: trigger.agentConfig?.maxSubagentDepth ?? 3,
|
|
1988
|
+
readFileState: new Map(),
|
|
1989
|
+
agentClient,
|
|
1990
|
+
modelId,
|
|
1991
|
+
startMs,
|
|
1992
|
+
},
|
|
1993
|
+
};
|
|
1994
|
+
}
|
|
1995
|
+
function constructTools(session, ctx, apiKey, schemas, scope) {
|
|
1996
|
+
const { state, sessionWorkspacePath, spawnCurrentDepth, spawnMaxDepth } = session;
|
|
1997
|
+
const { fileTracker, onAdvance, onComplete, emitter, abortRegistry, maxIssueSummaries } = scope;
|
|
1998
|
+
const sid = scope.sessionId;
|
|
1999
|
+
const workrailSid = scope.workrailSessionId;
|
|
2000
|
+
const readFileStateMap = fileTracker.toMap();
|
|
2001
|
+
return [
|
|
2002
|
+
makeCompleteStepTool(sid, ctx, () => state.currentContinueToken, onAdvance, onComplete, (t) => { state.currentContinueToken = t; }, schemas, index_js_1.executeContinueWorkflow, emitter, workrailSid),
|
|
2003
|
+
makeContinueWorkflowTool(sid, ctx, onAdvance, onComplete, schemas, index_js_1.executeContinueWorkflow, emitter, workrailSid),
|
|
2004
|
+
makeBashTool(sessionWorkspacePath, schemas, sid, emitter, workrailSid),
|
|
2005
|
+
makeReadTool(readFileStateMap, schemas, sid, emitter, workrailSid),
|
|
2006
|
+
makeWriteTool(readFileStateMap, schemas, sid, emitter, workrailSid),
|
|
2007
|
+
makeGlobTool(sessionWorkspacePath, schemas, sid, emitter, workrailSid),
|
|
2008
|
+
makeGrepTool(sessionWorkspacePath, schemas, sid, emitter, workrailSid),
|
|
2009
|
+
makeEditTool(sessionWorkspacePath, readFileStateMap, schemas, sid, emitter, workrailSid),
|
|
2010
|
+
makeReportIssueTool(sid, emitter, workrailSid, undefined, (summary) => {
|
|
2011
|
+
if (state.issueSummaries.length < maxIssueSummaries) {
|
|
1930
2012
|
state.issueSummaries.push(summary);
|
|
1931
2013
|
}
|
|
1932
2014
|
}),
|
|
1933
|
-
makeSpawnAgentTool(
|
|
1934
|
-
makeSignalCoordinatorTool(
|
|
2015
|
+
makeSpawnAgentTool(sid, ctx, apiKey, workrailSid ?? '', spawnCurrentDepth, spawnMaxDepth, runWorkflow, schemas, emitter, abortRegistry),
|
|
2016
|
+
makeSignalCoordinatorTool(sid, emitter, workrailSid),
|
|
1935
2017
|
];
|
|
1936
|
-
|
|
1937
|
-
|
|
1938
|
-
|
|
1939
|
-
|
|
1940
|
-
|
|
1941
|
-
|
|
1942
|
-
|
|
1943
|
-
|
|
1944
|
-
|
|
1945
|
-
|
|
1946
|
-
|
|
1947
|
-
|
|
2018
|
+
}
|
|
2019
|
+
function buildTurnEndSubscriber(ctx) {
|
|
2020
|
+
return async (event) => {
|
|
2021
|
+
if (event.type !== 'turn_end')
|
|
2022
|
+
return;
|
|
2023
|
+
for (const toolResult of event.toolResults) {
|
|
2024
|
+
if (toolResult.isError) {
|
|
2025
|
+
const errorText = toolResult.result?.content[0]?.text ?? 'tool error';
|
|
2026
|
+
ctx.emitter?.emit({ kind: 'tool_error', sessionId: ctx.sessionId, toolName: toolResult.toolName, error: errorText.slice(0, 200), ...withWorkrailSession(ctx.state.workrailSessionId) });
|
|
2027
|
+
}
|
|
2028
|
+
}
|
|
2029
|
+
ctx.state.turnCount++;
|
|
2030
|
+
const signal = evaluateStuckSignals(ctx.state, ctx.stuckConfig);
|
|
2031
|
+
if (signal !== null) {
|
|
2032
|
+
if (signal.kind === 'max_turns_exceeded') {
|
|
2033
|
+
ctx.state.timeoutReason = 'max_turns';
|
|
2034
|
+
ctx.emitter?.emit({ kind: 'agent_stuck', sessionId: ctx.sessionId, reason: 'timeout_imminent', detail: 'Max-turn limit reached', ...withWorkrailSession(ctx.state.workrailSessionId) });
|
|
2035
|
+
ctx.agent.abort();
|
|
2036
|
+
return;
|
|
2037
|
+
}
|
|
2038
|
+
else if (signal.kind === 'repeated_tool_call') {
|
|
2039
|
+
ctx.emitter?.emit({ kind: 'agent_stuck', sessionId: ctx.sessionId, reason: 'repeated_tool_call', detail: `Same tool+args called ${ctx.stuckRepeatThreshold} times: ${signal.toolName}`, toolName: signal.toolName, argsSummary: signal.argsSummary, ...withWorkrailSession(ctx.state.workrailSessionId) });
|
|
2040
|
+
void writeStuckOutboxEntry({ workflowId: ctx.workflowId, reason: 'repeated_tool_call', ...(ctx.state.issueSummaries.length > 0 ? { issueSummaries: [...ctx.state.issueSummaries] } : {}) });
|
|
2041
|
+
if (ctx.stuckConfig.stuckAbortPolicy !== 'notify_only' && ctx.state.stuckReason === null && ctx.state.timeoutReason === null) {
|
|
2042
|
+
ctx.state.stuckReason = 'repeated_tool_call';
|
|
2043
|
+
ctx.agent.abort();
|
|
2044
|
+
return;
|
|
2045
|
+
}
|
|
2046
|
+
}
|
|
2047
|
+
else if (signal.kind === 'no_progress') {
|
|
2048
|
+
ctx.emitter?.emit({ kind: 'agent_stuck', sessionId: ctx.sessionId, reason: 'no_progress', detail: `${signal.turnCount} turns used, 0 step advances (${signal.maxTurns} turn limit)`, ...withWorkrailSession(ctx.state.workrailSessionId) });
|
|
2049
|
+
if (ctx.stuckConfig.noProgressAbortEnabled) {
|
|
2050
|
+
void writeStuckOutboxEntry({ workflowId: ctx.workflowId, reason: 'no_progress', ...(ctx.state.issueSummaries.length > 0 ? { issueSummaries: [...ctx.state.issueSummaries] } : {}) });
|
|
2051
|
+
if (ctx.stuckConfig.stuckAbortPolicy !== 'notify_only' && ctx.state.stuckReason === null && ctx.state.timeoutReason === null) {
|
|
2052
|
+
ctx.state.stuckReason = 'no_progress';
|
|
2053
|
+
ctx.agent.abort();
|
|
2054
|
+
return;
|
|
2055
|
+
}
|
|
2056
|
+
}
|
|
2057
|
+
}
|
|
2058
|
+
else if (signal.kind === 'timeout_imminent') {
|
|
2059
|
+
ctx.emitter?.emit({ kind: 'agent_stuck', sessionId: ctx.sessionId, reason: 'timeout_imminent', detail: `${signal.timeoutReason === 'wall_clock' ? 'Wall-clock timeout' : 'Max-turn limit'} reached`, ...withWorkrailSession(ctx.state.workrailSessionId) });
|
|
2060
|
+
}
|
|
2061
|
+
else {
|
|
2062
|
+
(0, assert_never_js_1.assertNever)(signal);
|
|
2063
|
+
}
|
|
2064
|
+
}
|
|
2065
|
+
(0, conversation_flusher_js_1.flushConversation)(ctx.agent.state.messages, ctx.lastFlushedRef, ctx.conversationPath, appendConversationMessages);
|
|
2066
|
+
(0, step_injector_js_1.injectPendingSteps)(ctx.state, ctx.agent);
|
|
2067
|
+
};
|
|
2068
|
+
}
|
|
2069
|
+
function buildAgentCallbacks(sessionId, state, modelId, emitter, stuckRepeatThreshold) {
|
|
2070
|
+
return {
|
|
1948
2071
|
onLlmTurnStarted: ({ messageCount }) => {
|
|
1949
|
-
emitter?.emit({
|
|
1950
|
-
kind: 'llm_turn_started',
|
|
1951
|
-
sessionId,
|
|
1952
|
-
messageCount,
|
|
1953
|
-
modelId,
|
|
1954
|
-
...withWorkrailSession(state.workrailSessionId),
|
|
1955
|
-
});
|
|
2072
|
+
emitter?.emit({ kind: 'llm_turn_started', sessionId, messageCount, modelId, ...withWorkrailSession(state.workrailSessionId) });
|
|
1956
2073
|
},
|
|
1957
2074
|
onLlmTurnCompleted: ({ stopReason, outputTokens, inputTokens, toolNamesRequested }) => {
|
|
1958
|
-
emitter?.emit({
|
|
1959
|
-
kind: 'llm_turn_completed',
|
|
1960
|
-
sessionId,
|
|
1961
|
-
stopReason,
|
|
1962
|
-
outputTokens,
|
|
1963
|
-
inputTokens,
|
|
1964
|
-
toolNamesRequested,
|
|
1965
|
-
...withWorkrailSession(state.workrailSessionId),
|
|
1966
|
-
});
|
|
2075
|
+
emitter?.emit({ kind: 'llm_turn_completed', sessionId, stopReason, outputTokens, inputTokens, toolNamesRequested, ...withWorkrailSession(state.workrailSessionId) });
|
|
1967
2076
|
},
|
|
1968
2077
|
onToolCallStarted: ({ toolName, argsSummary }) => {
|
|
1969
2078
|
emitter?.emit({ kind: 'tool_call_started', sessionId, toolName, argsSummary, ...withWorkrailSession(state.workrailSessionId) });
|
|
1970
2079
|
state.lastNToolCalls.push({ toolName, argsSummary });
|
|
1971
|
-
if (state.lastNToolCalls.length >
|
|
2080
|
+
if (state.lastNToolCalls.length > stuckRepeatThreshold)
|
|
1972
2081
|
state.lastNToolCalls.shift();
|
|
1973
|
-
}
|
|
1974
2082
|
},
|
|
1975
2083
|
onToolCallCompleted: ({ toolName, durationMs, resultSummary }) => {
|
|
1976
2084
|
emitter?.emit({ kind: 'tool_call_completed', sessionId, toolName, durationMs, resultSummary, ...withWorkrailSession(state.workrailSessionId) });
|
|
@@ -1979,6 +2087,125 @@ async function runWorkflow(trigger, ctx, apiKey, daemonRegistry, emitter, steerR
|
|
|
1979
2087
|
emitter?.emit({ kind: 'tool_call_failed', sessionId, toolName, durationMs, errorMessage, ...withWorkrailSession(state.workrailSessionId) });
|
|
1980
2088
|
},
|
|
1981
2089
|
};
|
|
2090
|
+
}
|
|
2091
|
+
function buildSessionResult(state, stopReason, errorMessage, trigger, sessionId, sessionWorktreePath) {
|
|
2092
|
+
if (state.stuckReason !== null) {
|
|
2093
|
+
return {
|
|
2094
|
+
_tag: 'stuck',
|
|
2095
|
+
workflowId: trigger.workflowId,
|
|
2096
|
+
reason: state.stuckReason,
|
|
2097
|
+
message: `Session aborted: stuck heuristic fired (${state.stuckReason})`,
|
|
2098
|
+
stopReason: 'aborted',
|
|
2099
|
+
...(state.issueSummaries.length > 0 ? { issueSummaries: [...state.issueSummaries] } : {}),
|
|
2100
|
+
};
|
|
2101
|
+
}
|
|
2102
|
+
if (state.timeoutReason !== null) {
|
|
2103
|
+
const limitDescription = state.timeoutReason === 'wall_clock'
|
|
2104
|
+
? `${trigger.agentConfig?.maxSessionMinutes ?? exports.DEFAULT_SESSION_TIMEOUT_MINUTES} minutes`
|
|
2105
|
+
: `${trigger.agentConfig?.maxTurns ?? exports.DEFAULT_MAX_TURNS} turns`;
|
|
2106
|
+
return {
|
|
2107
|
+
_tag: 'timeout',
|
|
2108
|
+
workflowId: trigger.workflowId,
|
|
2109
|
+
reason: state.timeoutReason,
|
|
2110
|
+
message: `Workflow ${state.timeoutReason === 'wall_clock' ? 'timed out' : 'exceeded turn limit'} after ${limitDescription}`,
|
|
2111
|
+
stopReason: 'aborted',
|
|
2112
|
+
};
|
|
2113
|
+
}
|
|
2114
|
+
if (stopReason === 'error' || errorMessage) {
|
|
2115
|
+
const errMsg = errorMessage ?? 'Agent stopped with error reason';
|
|
2116
|
+
const lastToolCalled = state.lastNToolCalls.length > 0 ? state.lastNToolCalls[state.lastNToolCalls.length - 1] : null;
|
|
2117
|
+
const stuckMarker = `\n\nWORKTRAIN_STUCK: ${JSON.stringify({
|
|
2118
|
+
reason: 'session_error',
|
|
2119
|
+
error: errMsg.slice(0, 500),
|
|
2120
|
+
workflowId: trigger.workflowId,
|
|
2121
|
+
sessionId,
|
|
2122
|
+
turnCount: state.turnCount,
|
|
2123
|
+
stepAdvanceCount: state.stepAdvanceCount,
|
|
2124
|
+
...(lastToolCalled !== null && { lastToolCalled }),
|
|
2125
|
+
...(state.issueSummaries.length > 0 && { issueSummaries: state.issueSummaries }),
|
|
2126
|
+
})}`;
|
|
2127
|
+
return {
|
|
2128
|
+
_tag: 'error',
|
|
2129
|
+
workflowId: trigger.workflowId,
|
|
2130
|
+
message: errMsg,
|
|
2131
|
+
stopReason,
|
|
2132
|
+
lastStepNotes: stuckMarker,
|
|
2133
|
+
};
|
|
2134
|
+
}
|
|
2135
|
+
return {
|
|
2136
|
+
_tag: 'success',
|
|
2137
|
+
workflowId: trigger.workflowId,
|
|
2138
|
+
stopReason,
|
|
2139
|
+
...(state.lastStepNotes !== undefined ? { lastStepNotes: state.lastStepNotes } : {}),
|
|
2140
|
+
...(state.lastStepArtifacts !== undefined ? { lastStepArtifacts: state.lastStepArtifacts } : {}),
|
|
2141
|
+
...(sessionWorktreePath !== undefined ? { sessionWorkspacePath: sessionWorktreePath } : {}),
|
|
2142
|
+
...(sessionWorktreePath !== undefined ? { sessionId } : {}),
|
|
2143
|
+
...(trigger.botIdentity !== undefined ? { botIdentity: trigger.botIdentity } : {}),
|
|
2144
|
+
};
|
|
2145
|
+
}
|
|
2146
|
+
async function runWorkflow(trigger, ctx, apiKey, daemonRegistry, emitter, steerRegistry, abortRegistry, _statsDir, _sessionsDir) {
|
|
2147
|
+
const statsDir = _statsDir ?? DAEMON_STATS_DIR;
|
|
2148
|
+
const sessionsDir = _sessionsDir ?? exports.DAEMON_SESSIONS_DIR;
|
|
2149
|
+
const startMs = Date.now();
|
|
2150
|
+
const sessionId = (0, node_crypto_1.randomUUID)();
|
|
2151
|
+
console.log(`[WorkflowRunner] Session started: sessionId=${sessionId} workflowId=${trigger.workflowId}`);
|
|
2152
|
+
emitter?.emit({
|
|
2153
|
+
kind: 'session_started',
|
|
2154
|
+
sessionId,
|
|
2155
|
+
workflowId: trigger.workflowId,
|
|
2156
|
+
workspacePath: trigger.workspacePath,
|
|
2157
|
+
});
|
|
2158
|
+
const preResult = await buildPreAgentSession(trigger, ctx, apiKey, sessionId, startMs, statsDir, sessionsDir, emitter, daemonRegistry, steerRegistry);
|
|
2159
|
+
if (preResult.kind === 'complete') {
|
|
2160
|
+
return preResult.result;
|
|
2161
|
+
}
|
|
2162
|
+
const session = preResult.session;
|
|
2163
|
+
const { state, firstStep, sessionWorkspacePath, sessionWorktreePath, agentClient, modelId } = session;
|
|
2164
|
+
const startContinueToken = session.continueToken;
|
|
2165
|
+
const MAX_ISSUE_SUMMARIES = 10;
|
|
2166
|
+
const STUCK_REPEAT_THRESHOLD = 3;
|
|
2167
|
+
const onAdvance = (stepText, continueToken) => {
|
|
2168
|
+
state.pendingSteerParts.push(stepText);
|
|
2169
|
+
state.stepAdvanceCount++;
|
|
2170
|
+
state.currentContinueToken = continueToken;
|
|
2171
|
+
if (state.workrailSessionId !== null)
|
|
2172
|
+
daemonRegistry?.heartbeat(state.workrailSessionId);
|
|
2173
|
+
emitter?.emit({ kind: 'step_advanced', sessionId, ...withWorkrailSession(state.workrailSessionId) });
|
|
2174
|
+
};
|
|
2175
|
+
const onComplete = (notes, artifacts) => {
|
|
2176
|
+
state.isComplete = true;
|
|
2177
|
+
state.lastStepNotes = notes;
|
|
2178
|
+
state.lastStepArtifacts = artifacts;
|
|
2179
|
+
state.stepAdvanceCount++;
|
|
2180
|
+
if (state.workrailSessionId !== null)
|
|
2181
|
+
daemonRegistry?.heartbeat(state.workrailSessionId);
|
|
2182
|
+
emitter?.emit({ kind: 'step_advanced', sessionId, ...withWorkrailSession(state.workrailSessionId) });
|
|
2183
|
+
};
|
|
2184
|
+
const schemas = getSchemas();
|
|
2185
|
+
const scope = {
|
|
2186
|
+
fileTracker: new session_scope_js_1.DefaultFileStateTracker(session.readFileState),
|
|
2187
|
+
onAdvance,
|
|
2188
|
+
onComplete,
|
|
2189
|
+
workrailSessionId: state.workrailSessionId,
|
|
2190
|
+
emitter,
|
|
2191
|
+
sessionId,
|
|
2192
|
+
workflowId: trigger.workflowId,
|
|
2193
|
+
abortRegistry,
|
|
2194
|
+
maxIssueSummaries: MAX_ISSUE_SUMMARIES,
|
|
2195
|
+
};
|
|
2196
|
+
const tools = constructTools(session, ctx, apiKey, schemas, scope);
|
|
2197
|
+
const [soulContent, workspaceContext, sessionNotes] = await Promise.all([
|
|
2198
|
+
loadDaemonSoul(trigger.soulFile),
|
|
2199
|
+
loadWorkspaceContext(trigger.workspacePath),
|
|
2200
|
+
startContinueToken ? loadSessionNotes(startContinueToken, ctx) : Promise.resolve([]),
|
|
2201
|
+
]);
|
|
2202
|
+
const sessionCtx = buildSessionContext(trigger, {
|
|
2203
|
+
soulContent,
|
|
2204
|
+
workspaceContext,
|
|
2205
|
+
sessionNotes,
|
|
2206
|
+
firstStepPrompt: firstStep.pending?.prompt ?? 'No step content available',
|
|
2207
|
+
});
|
|
2208
|
+
const agentCallbacks = buildAgentCallbacks(sessionId, state, modelId, emitter, STUCK_REPEAT_THRESHOLD);
|
|
1982
2209
|
const agent = new agent_loop_js_1.AgentLoop({
|
|
1983
2210
|
systemPrompt: sessionCtx.systemPrompt,
|
|
1984
2211
|
modelId,
|
|
@@ -1990,6 +2217,9 @@ async function runWorkflow(trigger, ctx, apiKey, daemonRegistry, emitter, steerR
|
|
|
1990
2217
|
? { maxTokens: trigger.agentConfig.maxOutputTokens }
|
|
1991
2218
|
: {}),
|
|
1992
2219
|
});
|
|
2220
|
+
if (state.workrailSessionId !== null) {
|
|
2221
|
+
abortRegistry?.set(state.workrailSessionId, () => { agent.abort(); });
|
|
2222
|
+
}
|
|
1993
2223
|
const { sessionTimeoutMs, maxTurns } = sessionCtx;
|
|
1994
2224
|
const stuckConfig = {
|
|
1995
2225
|
maxTurns,
|
|
@@ -1998,96 +2228,18 @@ async function runWorkflow(trigger, ctx, apiKey, daemonRegistry, emitter, steerR
|
|
|
1998
2228
|
stuckRepeatThreshold: STUCK_REPEAT_THRESHOLD,
|
|
1999
2229
|
};
|
|
2000
2230
|
const conversationPath = path.join(sessionsDir, `${sessionId}-conversation.jsonl`);
|
|
2001
|
-
|
|
2002
|
-
const unsubscribe = agent.subscribe(
|
|
2003
|
-
|
|
2004
|
-
|
|
2005
|
-
|
|
2006
|
-
|
|
2007
|
-
|
|
2008
|
-
|
|
2009
|
-
|
|
2010
|
-
|
|
2011
|
-
|
|
2012
|
-
|
|
2013
|
-
if (signal !== null) {
|
|
2014
|
-
if (signal.kind === 'max_turns_exceeded') {
|
|
2015
|
-
state.timeoutReason = 'max_turns';
|
|
2016
|
-
emitter?.emit({
|
|
2017
|
-
kind: 'agent_stuck',
|
|
2018
|
-
sessionId,
|
|
2019
|
-
reason: 'timeout_imminent',
|
|
2020
|
-
detail: 'Max-turn limit reached',
|
|
2021
|
-
...withWorkrailSession(state.workrailSessionId),
|
|
2022
|
-
});
|
|
2023
|
-
agent.abort();
|
|
2024
|
-
return;
|
|
2025
|
-
}
|
|
2026
|
-
else if (signal.kind === 'repeated_tool_call') {
|
|
2027
|
-
emitter?.emit({
|
|
2028
|
-
kind: 'agent_stuck',
|
|
2029
|
-
sessionId,
|
|
2030
|
-
reason: 'repeated_tool_call',
|
|
2031
|
-
detail: `Same tool+args called ${STUCK_REPEAT_THRESHOLD} times: ${signal.toolName}`,
|
|
2032
|
-
toolName: signal.toolName,
|
|
2033
|
-
argsSummary: signal.argsSummary,
|
|
2034
|
-
...withWorkrailSession(state.workrailSessionId),
|
|
2035
|
-
});
|
|
2036
|
-
void writeStuckOutboxEntry({
|
|
2037
|
-
workflowId: trigger.workflowId,
|
|
2038
|
-
reason: 'repeated_tool_call',
|
|
2039
|
-
...(state.issueSummaries.length > 0 ? { issueSummaries: [...state.issueSummaries] } : {}),
|
|
2040
|
-
});
|
|
2041
|
-
if (stuckConfig.stuckAbortPolicy !== 'notify_only' && state.stuckReason === null && state.timeoutReason === null) {
|
|
2042
|
-
state.stuckReason = 'repeated_tool_call';
|
|
2043
|
-
agent.abort();
|
|
2044
|
-
return;
|
|
2045
|
-
}
|
|
2046
|
-
}
|
|
2047
|
-
else if (signal.kind === 'no_progress') {
|
|
2048
|
-
emitter?.emit({
|
|
2049
|
-
kind: 'agent_stuck',
|
|
2050
|
-
sessionId,
|
|
2051
|
-
reason: 'no_progress',
|
|
2052
|
-
detail: `${signal.turnCount} turns used, 0 step advances (${signal.maxTurns} turn limit)`,
|
|
2053
|
-
...withWorkrailSession(state.workrailSessionId),
|
|
2054
|
-
});
|
|
2055
|
-
if (stuckConfig.noProgressAbortEnabled) {
|
|
2056
|
-
void writeStuckOutboxEntry({
|
|
2057
|
-
workflowId: trigger.workflowId,
|
|
2058
|
-
reason: 'no_progress',
|
|
2059
|
-
...(state.issueSummaries.length > 0 ? { issueSummaries: [...state.issueSummaries] } : {}),
|
|
2060
|
-
});
|
|
2061
|
-
if (stuckConfig.stuckAbortPolicy !== 'notify_only' && state.stuckReason === null && state.timeoutReason === null) {
|
|
2062
|
-
state.stuckReason = 'no_progress';
|
|
2063
|
-
agent.abort();
|
|
2064
|
-
return;
|
|
2065
|
-
}
|
|
2066
|
-
}
|
|
2067
|
-
}
|
|
2068
|
-
else if (signal.kind === 'timeout_imminent') {
|
|
2069
|
-
emitter?.emit({
|
|
2070
|
-
kind: 'agent_stuck',
|
|
2071
|
-
sessionId,
|
|
2072
|
-
reason: 'timeout_imminent',
|
|
2073
|
-
detail: `${signal.timeoutReason === 'wall_clock' ? 'Wall-clock timeout' : 'Max-turn limit'} reached`,
|
|
2074
|
-
...withWorkrailSession(state.workrailSessionId),
|
|
2075
|
-
});
|
|
2076
|
-
}
|
|
2077
|
-
else {
|
|
2078
|
-
(0, assert_never_js_1.assertNever)(signal);
|
|
2079
|
-
}
|
|
2080
|
-
}
|
|
2081
|
-
const currentMessages = agent.state.messages;
|
|
2082
|
-
const newMessages = currentMessages.slice(lastFlushedMessageCount);
|
|
2083
|
-
lastFlushedMessageCount = currentMessages.length;
|
|
2084
|
-
void appendConversationMessages(conversationPath, newMessages).catch(() => { });
|
|
2085
|
-
if (state.pendingSteerParts.length > 0 && !state.isComplete) {
|
|
2086
|
-
const joined = state.pendingSteerParts.join('\n\n');
|
|
2087
|
-
state.pendingSteerParts.length = 0;
|
|
2088
|
-
agent.steer(buildUserMessage(joined));
|
|
2089
|
-
}
|
|
2090
|
-
});
|
|
2231
|
+
const lastFlushedRef = { count: 0 };
|
|
2232
|
+
const unsubscribe = agent.subscribe(buildTurnEndSubscriber({
|
|
2233
|
+
agent,
|
|
2234
|
+
state,
|
|
2235
|
+
stuckConfig,
|
|
2236
|
+
sessionId,
|
|
2237
|
+
workflowId: trigger.workflowId,
|
|
2238
|
+
emitter,
|
|
2239
|
+
conversationPath,
|
|
2240
|
+
lastFlushedRef,
|
|
2241
|
+
stuckRepeatThreshold: STUCK_REPEAT_THRESHOLD,
|
|
2242
|
+
}));
|
|
2091
2243
|
let stopReason = 'stop';
|
|
2092
2244
|
let errorMessage;
|
|
2093
2245
|
let timeoutHandle;
|
|
@@ -2124,7 +2276,7 @@ async function runWorkflow(trigger, ctx, apiKey, daemonRegistry, emitter, steerR
|
|
|
2124
2276
|
}
|
|
2125
2277
|
finally {
|
|
2126
2278
|
unsubscribe();
|
|
2127
|
-
const remainingMessages = agent.state.messages.slice(
|
|
2279
|
+
const remainingMessages = agent.state.messages.slice(lastFlushedRef.count);
|
|
2128
2280
|
void appendConversationMessages(conversationPath, remainingMessages).catch(() => { });
|
|
2129
2281
|
if (timeoutHandle !== undefined)
|
|
2130
2282
|
clearTimeout(timeoutHandle);
|
|
@@ -2149,65 +2301,7 @@ async function runWorkflow(trigger, ctx, apiKey, daemonRegistry, emitter, steerR
|
|
|
2149
2301
|
daemonRegistry,
|
|
2150
2302
|
workflowId: trigger.workflowId,
|
|
2151
2303
|
};
|
|
2152
|
-
|
|
2153
|
-
const result = {
|
|
2154
|
-
_tag: 'stuck',
|
|
2155
|
-
workflowId: trigger.workflowId,
|
|
2156
|
-
reason: state.stuckReason,
|
|
2157
|
-
message: `Session aborted: stuck heuristic fired (${state.stuckReason})`,
|
|
2158
|
-
stopReason: 'aborted',
|
|
2159
|
-
...(state.issueSummaries.length > 0 ? { issueSummaries: [...state.issueSummaries] } : {}),
|
|
2160
|
-
};
|
|
2161
|
-
await finalizeSession(result, finalizationCtx);
|
|
2162
|
-
return result;
|
|
2163
|
-
}
|
|
2164
|
-
if (state.timeoutReason !== null) {
|
|
2165
|
-
const limitDescription = state.timeoutReason === 'wall_clock'
|
|
2166
|
-
? `${trigger.agentConfig?.maxSessionMinutes ?? exports.DEFAULT_SESSION_TIMEOUT_MINUTES} minutes`
|
|
2167
|
-
: `${trigger.agentConfig?.maxTurns ?? exports.DEFAULT_MAX_TURNS} turns`;
|
|
2168
|
-
const result = {
|
|
2169
|
-
_tag: 'timeout',
|
|
2170
|
-
workflowId: trigger.workflowId,
|
|
2171
|
-
reason: state.timeoutReason,
|
|
2172
|
-
message: `Workflow ${state.timeoutReason === 'wall_clock' ? 'timed out' : 'exceeded turn limit'} after ${limitDescription}`,
|
|
2173
|
-
stopReason: 'aborted',
|
|
2174
|
-
};
|
|
2175
|
-
await finalizeSession(result, finalizationCtx);
|
|
2176
|
-
return result;
|
|
2177
|
-
}
|
|
2178
|
-
if (stopReason === 'error' || errorMessage) {
|
|
2179
|
-
const errMsg = errorMessage ?? 'Agent stopped with error reason';
|
|
2180
|
-
const lastToolCalled = state.lastNToolCalls.length > 0 ? state.lastNToolCalls[state.lastNToolCalls.length - 1] : null;
|
|
2181
|
-
const stuckMarker = `\n\nWORKTRAIN_STUCK: ${JSON.stringify({
|
|
2182
|
-
reason: 'session_error',
|
|
2183
|
-
error: errMsg.slice(0, 500),
|
|
2184
|
-
workflowId: trigger.workflowId,
|
|
2185
|
-
sessionId,
|
|
2186
|
-
turnCount: state.turnCount,
|
|
2187
|
-
stepAdvanceCount: state.stepAdvanceCount,
|
|
2188
|
-
...(lastToolCalled !== null && { lastToolCalled }),
|
|
2189
|
-
...(state.issueSummaries.length > 0 && { issueSummaries: state.issueSummaries }),
|
|
2190
|
-
})}`;
|
|
2191
|
-
const result = {
|
|
2192
|
-
_tag: 'error',
|
|
2193
|
-
workflowId: trigger.workflowId,
|
|
2194
|
-
message: errMsg,
|
|
2195
|
-
stopReason,
|
|
2196
|
-
lastStepNotes: stuckMarker,
|
|
2197
|
-
};
|
|
2198
|
-
await finalizeSession(result, finalizationCtx);
|
|
2199
|
-
return result;
|
|
2200
|
-
}
|
|
2201
|
-
const result = {
|
|
2202
|
-
_tag: 'success',
|
|
2203
|
-
workflowId: trigger.workflowId,
|
|
2204
|
-
stopReason,
|
|
2205
|
-
...(state.lastStepNotes !== undefined ? { lastStepNotes: state.lastStepNotes } : {}),
|
|
2206
|
-
...(state.lastStepArtifacts !== undefined ? { lastStepArtifacts: state.lastStepArtifacts } : {}),
|
|
2207
|
-
...(sessionWorktreePath !== undefined ? { sessionWorkspacePath: sessionWorktreePath } : {}),
|
|
2208
|
-
...(sessionWorktreePath !== undefined ? { sessionId } : {}),
|
|
2209
|
-
...(trigger.botIdentity !== undefined ? { botIdentity: trigger.botIdentity } : {}),
|
|
2210
|
-
};
|
|
2304
|
+
const result = buildSessionResult(state, stopReason, errorMessage, trigger, sessionId, sessionWorktreePath);
|
|
2211
2305
|
await finalizeSession(result, finalizationCtx);
|
|
2212
2306
|
return result;
|
|
2213
2307
|
}
|