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