@exaudeus/workrail 3.71.1 → 3.72.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.
Files changed (50) hide show
  1. package/dist/cli-worktrain.js +4 -6
  2. package/dist/console-ui/assets/{index-CsX-nVV7.js → index-Yj9NHqbR.js} +1 -1
  3. package/dist/console-ui/index.html +1 -1
  4. package/dist/daemon/active-sessions.d.ts +17 -0
  5. package/dist/daemon/active-sessions.js +55 -0
  6. package/dist/daemon/context-loader.d.ts +32 -0
  7. package/dist/daemon/context-loader.js +34 -0
  8. package/dist/daemon/session-scope.d.ts +28 -0
  9. package/dist/daemon/session-scope.js +21 -0
  10. package/dist/daemon/tools/_shared.d.ts +38 -0
  11. package/dist/daemon/tools/_shared.js +101 -0
  12. package/dist/daemon/tools/bash.d.ts +3 -0
  13. package/dist/daemon/tools/bash.js +57 -0
  14. package/dist/daemon/tools/continue-workflow.d.ts +6 -0
  15. package/dist/daemon/tools/continue-workflow.js +208 -0
  16. package/dist/daemon/tools/file-tools.d.ts +6 -0
  17. package/dist/daemon/tools/file-tools.js +195 -0
  18. package/dist/daemon/tools/glob-grep.d.ts +4 -0
  19. package/dist/daemon/tools/glob-grep.js +172 -0
  20. package/dist/daemon/tools/report-issue.d.ts +3 -0
  21. package/dist/daemon/tools/report-issue.js +129 -0
  22. package/dist/daemon/tools/signal-coordinator.d.ts +4 -0
  23. package/dist/daemon/tools/signal-coordinator.js +105 -0
  24. package/dist/daemon/tools/spawn-agent.d.ts +6 -0
  25. package/dist/daemon/tools/spawn-agent.js +135 -0
  26. package/dist/daemon/turn-end/conversation-flusher.d.ts +4 -0
  27. package/dist/daemon/turn-end/conversation-flusher.js +8 -0
  28. package/dist/daemon/turn-end/detect-stuck.d.ts +2 -0
  29. package/dist/daemon/turn-end/detect-stuck.js +5 -0
  30. package/dist/daemon/turn-end/step-injector.d.ts +8 -0
  31. package/dist/daemon/turn-end/step-injector.js +10 -0
  32. package/dist/daemon/workflow-runner.d.ts +54 -29
  33. package/dist/daemon/workflow-runner.js +175 -989
  34. package/dist/infrastructure/storage/workflow-resolution.js +5 -6
  35. package/dist/manifest.json +161 -25
  36. package/dist/mcp/handlers/shared/request-workflow-reader.js +14 -0
  37. package/dist/trigger/coordinator-deps.d.ts +15 -0
  38. package/dist/trigger/coordinator-deps.js +322 -0
  39. package/dist/trigger/delivery-pipeline.d.ts +18 -0
  40. package/dist/trigger/delivery-pipeline.js +148 -0
  41. package/dist/trigger/dispatch-deduplicator.d.ts +6 -0
  42. package/dist/trigger/dispatch-deduplicator.js +24 -0
  43. package/dist/trigger/trigger-listener.d.ts +2 -3
  44. package/dist/trigger/trigger-listener.js +9 -276
  45. package/dist/trigger/trigger-router.d.ts +8 -7
  46. package/dist/trigger/trigger-router.js +19 -97
  47. package/dist/v2/usecases/console-routes.js +10 -2
  48. package/docs/ideas/backlog.md +82 -48
  49. package/package.json +1 -1
  50. package/workflows/wr.research.json +158 -0
@@ -36,7 +36,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
36
36
  return (mod && mod.__esModule) ? mod : { "default": mod };
37
37
  };
38
38
  Object.defineProperty(exports, "__esModule", { value: true });
39
- exports.DAEMON_SIGNALS_DIR = exports.DAEMON_SOUL_TEMPLATE = exports.DAEMON_SOUL_DEFAULT = exports.WORKTREES_DIR = exports.DAEMON_SESSIONS_DIR = exports.DEFAULT_MAX_TURNS = exports.DEFAULT_SESSION_TIMEOUT_MINUTES = void 0;
39
+ exports.DAEMON_SOUL_TEMPLATE = exports.DAEMON_SOUL_DEFAULT = exports.WORKTREES_DIR = exports.DEFAULT_MAX_TURNS = exports.DEFAULT_SESSION_TIMEOUT_MINUTES = exports.makeSignalCoordinatorTool = exports.makeReportIssueTool = exports.makeSpawnAgentTool = exports.makeGrepTool = exports.makeGlobTool = exports.makeEditTool = exports.makeWriteTool = exports.makeReadTool = exports.makeBashTool = exports.makeCompleteStepTool = exports.makeContinueWorkflowTool = exports.DAEMON_SIGNALS_DIR = exports.DAEMON_SESSIONS_DIR = void 0;
40
40
  exports.readDaemonSessionState = readDaemonSessionState;
41
41
  exports.readAllDaemonSessions = readAllDaemonSessions;
42
42
  exports.runStartupRecovery = runStartupRecovery;
@@ -45,17 +45,6 @@ exports.clearQueueIssueSidecars = clearQueueIssueSidecars;
45
45
  exports.stripFrontmatter = stripFrontmatter;
46
46
  exports.loadWorkspaceContext = loadWorkspaceContext;
47
47
  exports.loadSessionNotes = loadSessionNotes;
48
- exports.makeContinueWorkflowTool = makeContinueWorkflowTool;
49
- exports.makeCompleteStepTool = makeCompleteStepTool;
50
- exports.makeBashTool = makeBashTool;
51
- exports.makeReadTool = makeReadTool;
52
- exports.makeWriteTool = makeWriteTool;
53
- exports.makeGlobTool = makeGlobTool;
54
- exports.makeGrepTool = makeGrepTool;
55
- exports.makeEditTool = makeEditTool;
56
- exports.makeSpawnAgentTool = makeSpawnAgentTool;
57
- exports.makeReportIssueTool = makeReportIssueTool;
58
- exports.makeSignalCoordinatorTool = makeSignalCoordinatorTool;
59
48
  exports.buildSessionRecap = buildSessionRecap;
60
49
  exports.buildSystemPrompt = buildSystemPrompt;
61
50
  exports.tagToStatsOutcome = tagToStatsOutcome;
@@ -87,20 +76,40 @@ const v2_token_ops_js_1 = require("../mcp/handlers/v2-token-ops.js");
87
76
  const index_js_2 = require("../v2/durable-core/ids/index.js");
88
77
  const node_outputs_js_1 = require("../v2/projections/node-outputs.js");
89
78
  const assert_never_js_1 = require("../runtime/assert-never.js");
90
- const result_js_1 = require("../runtime/result.js");
91
79
  const session_recovery_policy_js_1 = require("./session-recovery-policy.js");
92
80
  const stats_summary_js_1 = require("./stats-summary.js");
93
- const execAsync = (0, node_util_1.promisify)(node_child_process_1.exec);
81
+ const step_injector_js_1 = require("./turn-end/step-injector.js");
82
+ const conversation_flusher_js_1 = require("./turn-end/conversation-flusher.js");
83
+ const session_scope_js_1 = require("./session-scope.js");
84
+ const context_loader_js_1 = require("./context-loader.js");
85
+ const _shared_js_1 = require("./tools/_shared.js");
86
+ const continue_workflow_js_1 = require("./tools/continue-workflow.js");
87
+ Object.defineProperty(exports, "makeContinueWorkflowTool", { enumerable: true, get: function () { return continue_workflow_js_1.makeContinueWorkflowTool; } });
88
+ Object.defineProperty(exports, "makeCompleteStepTool", { enumerable: true, get: function () { return continue_workflow_js_1.makeCompleteStepTool; } });
89
+ const bash_js_1 = require("./tools/bash.js");
90
+ Object.defineProperty(exports, "makeBashTool", { enumerable: true, get: function () { return bash_js_1.makeBashTool; } });
91
+ const file_tools_js_1 = require("./tools/file-tools.js");
92
+ Object.defineProperty(exports, "makeReadTool", { enumerable: true, get: function () { return file_tools_js_1.makeReadTool; } });
93
+ Object.defineProperty(exports, "makeWriteTool", { enumerable: true, get: function () { return file_tools_js_1.makeWriteTool; } });
94
+ Object.defineProperty(exports, "makeEditTool", { enumerable: true, get: function () { return file_tools_js_1.makeEditTool; } });
95
+ const glob_grep_js_1 = require("./tools/glob-grep.js");
96
+ Object.defineProperty(exports, "makeGlobTool", { enumerable: true, get: function () { return glob_grep_js_1.makeGlobTool; } });
97
+ Object.defineProperty(exports, "makeGrepTool", { enumerable: true, get: function () { return glob_grep_js_1.makeGrepTool; } });
98
+ const spawn_agent_js_1 = require("./tools/spawn-agent.js");
99
+ Object.defineProperty(exports, "makeSpawnAgentTool", { enumerable: true, get: function () { return spawn_agent_js_1.makeSpawnAgentTool; } });
100
+ const report_issue_js_1 = require("./tools/report-issue.js");
101
+ Object.defineProperty(exports, "makeReportIssueTool", { enumerable: true, get: function () { return report_issue_js_1.makeReportIssueTool; } });
102
+ const signal_coordinator_js_1 = require("./tools/signal-coordinator.js");
103
+ Object.defineProperty(exports, "makeSignalCoordinatorTool", { enumerable: true, get: function () { return signal_coordinator_js_1.makeSignalCoordinatorTool; } });
104
+ var _shared_js_2 = require("./tools/_shared.js");
105
+ Object.defineProperty(exports, "DAEMON_SESSIONS_DIR", { enumerable: true, get: function () { return _shared_js_2.DAEMON_SESSIONS_DIR; } });
106
+ var signal_coordinator_js_2 = require("./tools/signal-coordinator.js");
107
+ Object.defineProperty(exports, "DAEMON_SIGNALS_DIR", { enumerable: true, get: function () { return signal_coordinator_js_2.DAEMON_SIGNALS_DIR; } });
94
108
  const execFileAsync = (0, node_util_1.promisify)(node_child_process_1.execFile);
95
- const BASH_TIMEOUT_MS = 5 * 60 * 1000;
96
109
  const MAX_SESSION_RECAP_NOTES = 3;
97
110
  const MAX_SESSION_NOTE_CHARS = 800;
98
111
  exports.DEFAULT_SESSION_TIMEOUT_MINUTES = 30;
99
112
  exports.DEFAULT_MAX_TURNS = 200;
100
- function withWorkrailSession(sid) {
101
- return sid != null ? { workrailSessionId: sid } : {};
102
- }
103
- exports.DAEMON_SESSIONS_DIR = path.join(os.homedir(), '.workrail', 'daemon-sessions');
104
113
  const MAX_ORPHAN_AGE_MS = 2 * 60 * 60 * 1000;
105
114
  const MAX_WORKTREE_ORPHAN_AGE_MS = 24 * 60 * 60 * 1000;
106
115
  const WORKRAIL_DIR = path.join(os.homedir(), '.workrail');
@@ -127,40 +136,15 @@ const soul_template_js_1 = require("./soul-template.js");
127
136
  var soul_template_js_2 = require("./soul-template.js");
128
137
  Object.defineProperty(exports, "DAEMON_SOUL_DEFAULT", { enumerable: true, get: function () { return soul_template_js_2.DAEMON_SOUL_DEFAULT; } });
129
138
  Object.defineProperty(exports, "DAEMON_SOUL_TEMPLATE", { enumerable: true, get: function () { return soul_template_js_2.DAEMON_SOUL_TEMPLATE; } });
130
- async function persistTokens(sessionId, continueToken, checkpointToken, worktreePath, recoveryContext) {
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
- }
154
- }
155
139
  async function appendConversationMessages(filePath, messages) {
156
140
  if (messages.length === 0)
157
141
  return;
158
142
  const lines = messages.map((m) => JSON.stringify(m)).join('\n') + '\n';
159
- await fs.mkdir(exports.DAEMON_SESSIONS_DIR, { recursive: true });
143
+ await fs.mkdir(_shared_js_1.DAEMON_SESSIONS_DIR, { recursive: true });
160
144
  await fs.appendFile(filePath, lines, 'utf8');
161
145
  }
162
146
  async function readDaemonSessionState(sessionId) {
163
- const sessionPath = path.join(exports.DAEMON_SESSIONS_DIR, `${sessionId}.json`);
147
+ const sessionPath = path.join(_shared_js_1.DAEMON_SESSIONS_DIR, `${sessionId}.json`);
164
148
  try {
165
149
  const raw = await fs.readFile(sessionPath, 'utf8');
166
150
  const parsed = JSON.parse(raw);
@@ -170,7 +154,7 @@ async function readDaemonSessionState(sessionId) {
170
154
  return null;
171
155
  }
172
156
  }
173
- async function readAllDaemonSessions(sessionsDir = exports.DAEMON_SESSIONS_DIR) {
157
+ async function readAllDaemonSessions(sessionsDir = _shared_js_1.DAEMON_SESSIONS_DIR) {
174
158
  let entries;
175
159
  try {
176
160
  entries = await fs.readdir(sessionsDir);
@@ -212,7 +196,7 @@ async function readAllDaemonSessions(sessionsDir = exports.DAEMON_SESSIONS_DIR)
212
196
  }
213
197
  return sessions;
214
198
  }
215
- async function runStartupRecovery(sessionsDir = exports.DAEMON_SESSIONS_DIR, execFn = execFileAsync, ctx, _countStepAdvancesFn = countOrphanStepAdvances, _executeContinueWorkflowFn = index_js_1.executeContinueWorkflow, _runWorkflowFn = runWorkflow, apiKey = '') {
199
+ async function runStartupRecovery(sessionsDir = _shared_js_1.DAEMON_SESSIONS_DIR, execFn = execFileAsync, ctx, _countStepAdvancesFn = countOrphanStepAdvances, _executeContinueWorkflowFn = index_js_1.executeContinueWorkflow, _runWorkflowFn = runWorkflow, apiKey = '') {
216
200
  await clearQueueIssueSidecars(sessionsDir);
217
201
  const sessions = await readAllDaemonSessions(sessionsDir);
218
202
  if (sessions.length === 0) {
@@ -301,14 +285,12 @@ async function runStartupRecovery(sessionsDir = exports.DAEMON_SESSIONS_DIR, exe
301
285
  `or has no pending step. Discarding.`);
302
286
  break;
303
287
  }
304
- const preAllocated = {
288
+ const recoveryAllocatedSession = {
305
289
  continueToken: rehydrated.continueToken ?? '',
306
290
  checkpointToken: rehydrated.checkpointToken,
291
+ firstStepPrompt: rehydrated.pending.prompt ?? '',
307
292
  isComplete: rehydrated.isComplete,
308
- pending: rehydrated.pending,
309
- preferences: rehydrated.preferences,
310
- nextIntent: rehydrated.nextIntent,
311
- nextCall: rehydrated.nextCall,
293
+ triggerSource: 'daemon',
312
294
  };
313
295
  const effectiveWorkspacePath = session.worktreePath ?? session.workspacePath;
314
296
  const branchStrategy = 'none';
@@ -317,11 +299,15 @@ async function runStartupRecovery(sessionsDir = exports.DAEMON_SESSIONS_DIR, exe
317
299
  goal: session.goal ?? 'Resumed session (crash recovery)',
318
300
  workspacePath: effectiveWorkspacePath,
319
301
  branchStrategy,
320
- _preAllocatedStartResponse: preAllocated,
302
+ };
303
+ const recoverySource = {
304
+ kind: 'pre_allocated',
305
+ trigger: recoveredTrigger,
306
+ session: recoveryAllocatedSession,
321
307
  };
322
308
  console.log(`[WorkflowRunner] Startup recovery: resuming session ${session.sessionId} ` +
323
309
  `workflowId=${session.workflowId} stepAdvances=${stepAdvances}`);
324
- void _runWorkflowFn(recoveredTrigger, ctx, apiKey).then((result) => {
310
+ void _runWorkflowFn(recoveredTrigger, ctx, apiKey, undefined, undefined, undefined, undefined, undefined, recoverySource).then((result) => {
325
311
  console.log(`[WorkflowRunner] Startup recovery: resumed session ${session.sessionId} completed: ${result._tag}`);
326
312
  }).catch((err) => {
327
313
  console.warn(`[WorkflowRunner] Startup recovery: resumed session ${session.sessionId} failed: ` +
@@ -710,673 +696,6 @@ function getSchemas() {
710
696
  };
711
697
  return _schemas;
712
698
  }
713
- function makeContinueWorkflowTool(sessionId, ctx, onAdvance, onComplete, schemas, _executeContinueWorkflowFn = index_js_1.executeContinueWorkflow, emitter, workrailSessionId) {
714
- return {
715
- name: 'continue_workflow',
716
- description: '[DEPRECATED in daemon sessions -- use complete_step instead] ' +
717
- 'Advance the WorkRail workflow to the next step. Call this after completing all work ' +
718
- 'required by the current step. Include your notes in notesMarkdown. ' +
719
- 'When the step requires an assessment gate, include wr.assessment objects in artifacts.',
720
- inputSchema: schemas['ContinueWorkflowParams'],
721
- label: 'Continue Workflow',
722
- execute: async (_toolCallId, params) => {
723
- console.log(`[WorkflowRunner] Tool: continue_workflow sessionId=${sessionId}`);
724
- emitter?.emit({ kind: 'tool_called', sessionId, toolName: 'continue_workflow', summary: params.intent ?? 'advance', ...withWorkrailSession(workrailSessionId) });
725
- const result = await _executeContinueWorkflowFn({
726
- continueToken: params.continueToken,
727
- intent: (params.intent ?? 'advance'),
728
- output: (params.notesMarkdown || params.artifacts?.length)
729
- ? {
730
- ...(params.notesMarkdown ? { notesMarkdown: params.notesMarkdown } : {}),
731
- ...(params.artifacts ? { artifacts: params.artifacts } : {}),
732
- }
733
- : undefined,
734
- context: params.context,
735
- }, ctx);
736
- if (result.isErr()) {
737
- throw new Error(`continue_workflow failed: ${result.error.kind} -- ${JSON.stringify(result.error)}`);
738
- }
739
- const out = result.value.response;
740
- const continueToken = out.continueToken ?? '';
741
- const checkpointToken = out.checkpointToken ?? null;
742
- const persistToken = (out.kind === 'blocked' ? out.nextCall?.params.continueToken : undefined) ?? continueToken;
743
- if (persistToken) {
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
- }
748
- }
749
- if (out.kind === 'blocked') {
750
- const retryToken = out.nextCall?.params.continueToken ?? continueToken;
751
- const lines = ['## Step blocked -- action required\n'];
752
- for (const blocker of out.blockers.blockers) {
753
- lines.push(blocker.message);
754
- if (blocker.suggestedFix) {
755
- lines.push(`\nWhat to do: ${blocker.suggestedFix}`);
756
- }
757
- lines.push('');
758
- }
759
- if (out.validation) {
760
- if (out.validation.issues.length > 0) {
761
- lines.push('**Issues:**');
762
- for (const issue of out.validation.issues)
763
- lines.push(`- ${issue}`);
764
- lines.push('');
765
- }
766
- if (out.validation.suggestions.length > 0) {
767
- lines.push('**Suggestions:**');
768
- for (const s of out.validation.suggestions)
769
- lines.push(`- ${s}`);
770
- lines.push('');
771
- }
772
- }
773
- if (out.assessmentFollowup) {
774
- lines.push(`**Follow-up required:** ${out.assessmentFollowup.title}`);
775
- lines.push(out.assessmentFollowup.guidance);
776
- lines.push('');
777
- }
778
- if (out.retryable) {
779
- lines.push(`Retry the same step with corrected output.\n\ncontinueToken: ${retryToken}`);
780
- }
781
- else {
782
- lines.push(`You cannot proceed without resolving this. Inform the user and wait for their response, then call continue_workflow.\n\ncontinueToken: ${retryToken}`);
783
- }
784
- const feedback = lines.join('\n');
785
- return {
786
- content: [{ type: 'text', text: feedback }],
787
- details: out,
788
- };
789
- }
790
- if (out.isComplete) {
791
- onComplete(params.notesMarkdown, Array.isArray(params.artifacts) ? params.artifacts : undefined);
792
- return {
793
- content: [{ type: 'text', text: 'Workflow complete. All steps have been executed.' }],
794
- details: out,
795
- };
796
- }
797
- const pending = out.pending;
798
- const stepText = pending
799
- ? `## Next step: ${pending.title}\n\n${pending.prompt}\n\ncontinueToken: ${continueToken}`
800
- : `Step advanced. continueToken: ${continueToken}`;
801
- onAdvance(stepText, continueToken);
802
- return {
803
- content: [{ type: 'text', text: stepText }],
804
- details: out,
805
- };
806
- },
807
- };
808
- }
809
- function makeCompleteStepTool(sessionId, ctx, getCurrentToken, onAdvance, onComplete, onTokenUpdate, schemas, _executeContinueWorkflowFn = index_js_1.executeContinueWorkflow, emitter, workrailSessionId) {
810
- return {
811
- name: 'complete_step',
812
- description: 'Mark the current WorkRail workflow step as complete and advance to the next one. ' +
813
- 'Call this after completing all work required by the current step. ' +
814
- 'Include your substantive notes (min 50 characters) describing what you did. ' +
815
- 'The daemon manages the session token internally -- you do not need a continueToken. ' +
816
- 'When the step requires an assessment gate, include wr.assessment objects in artifacts.',
817
- inputSchema: schemas['CompleteStepParams'],
818
- label: 'Complete Step',
819
- execute: async (_toolCallId, params) => {
820
- console.log(`[WorkflowRunner] Tool: complete_step sessionId=${sessionId}`);
821
- emitter?.emit({ kind: 'tool_called', sessionId, toolName: 'complete_step', summary: 'advance', ...withWorkrailSession(workrailSessionId) });
822
- const notes = params.notes;
823
- if (!notes || notes.length < 50) {
824
- throw new Error(`complete_step: notes is required and must be at least 50 characters. ` +
825
- `Provide substantive notes describing what you did, what you produced, and any notable decisions. ` +
826
- `Current length: ${notes?.length ?? 0} characters.`);
827
- }
828
- const continueToken = getCurrentToken();
829
- const result = await _executeContinueWorkflowFn({
830
- continueToken,
831
- intent: 'advance',
832
- output: (notes || params.artifacts?.length)
833
- ? {
834
- notesMarkdown: notes,
835
- ...(params.artifacts?.length ? { artifacts: params.artifacts } : {}),
836
- }
837
- : undefined,
838
- context: params.context,
839
- }, ctx);
840
- if (result.isErr()) {
841
- throw new Error(`complete_step failed: ${result.error.kind} -- ${JSON.stringify(result.error)}`);
842
- }
843
- const out = result.value.response;
844
- const newContinueToken = out.continueToken ?? '';
845
- const checkpointToken = out.checkpointToken ?? null;
846
- const persistToken = (out.kind === 'blocked' ? out.nextCall?.params.continueToken : undefined) ?? newContinueToken;
847
- if (persistToken) {
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
- }
852
- }
853
- if (out.kind === 'blocked') {
854
- const retryToken = out.nextCall?.params.continueToken ?? newContinueToken;
855
- onTokenUpdate(retryToken);
856
- const lines = ['## Step blocked -- action required\n'];
857
- for (const blocker of out.blockers.blockers) {
858
- lines.push(blocker.message);
859
- if (blocker.suggestedFix) {
860
- lines.push(`\nWhat to do: ${blocker.suggestedFix}`);
861
- }
862
- lines.push('');
863
- }
864
- if (out.validation) {
865
- if (out.validation.issues.length > 0) {
866
- lines.push('**Issues:**');
867
- for (const issue of out.validation.issues)
868
- lines.push(`- ${issue}`);
869
- lines.push('');
870
- }
871
- if (out.validation.suggestions.length > 0) {
872
- lines.push('**Suggestions:**');
873
- for (const s of out.validation.suggestions)
874
- lines.push(`- ${s}`);
875
- lines.push('');
876
- }
877
- }
878
- if (out.assessmentFollowup) {
879
- lines.push(`**Follow-up required:** ${out.assessmentFollowup.title}`);
880
- lines.push(out.assessmentFollowup.guidance);
881
- lines.push('');
882
- }
883
- if (out.retryable) {
884
- lines.push(`Retry the same step: call complete_step again with corrected notes.`);
885
- }
886
- else {
887
- lines.push(`You cannot proceed without resolving this. Inform the user and wait for their response, then call complete_step.`);
888
- }
889
- const feedback = lines.join('\n');
890
- return {
891
- content: [{ type: 'text', text: feedback }],
892
- details: out,
893
- };
894
- }
895
- if (out.isComplete) {
896
- onComplete(notes, Array.isArray(params.artifacts) ? params.artifacts : undefined);
897
- return {
898
- content: [{ type: 'text', text: JSON.stringify({ status: 'complete' }) }],
899
- details: out,
900
- };
901
- }
902
- const pending = out.pending;
903
- const nextStepTitle = pending?.title ?? 'Next step';
904
- const stepText = pending
905
- ? `${JSON.stringify({ status: 'advanced', nextStep: pending.title })}\n\n## ${pending.title}\n\n${pending.prompt}`
906
- : JSON.stringify({ status: 'advanced', nextStep: nextStepTitle });
907
- onAdvance(stepText, newContinueToken);
908
- return {
909
- content: [{ type: 'text', text: stepText }],
910
- details: out,
911
- };
912
- },
913
- };
914
- }
915
- function makeBashTool(workspacePath, schemas, sessionId, emitter, workrailSessionId) {
916
- return {
917
- name: 'Bash',
918
- description: 'Execute a shell command. Throws on failure (non-zero exit with stderr, or exit code 2+). ' +
919
- 'Exit code 1 with empty stderr is treated as "no match found" (standard grep semantics) and ' +
920
- 'returns empty output without throwing. ' +
921
- `Maximum execution time: ${BASH_TIMEOUT_MS / 1000}s.`,
922
- inputSchema: schemas['BashParams'],
923
- label: 'Bash',
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');
927
- console.log(`[WorkflowRunner] Tool: bash "${String(params.command).slice(0, 80)}"`);
928
- if (sessionId)
929
- emitter?.emit({ kind: 'tool_called', sessionId, toolName: 'Bash', summary: String(params.command).slice(0, 80), ...withWorkrailSession(workrailSessionId) });
930
- const cwd = params.cwd ?? workspacePath;
931
- try {
932
- const { stdout, stderr } = await execAsync(params.command, {
933
- cwd,
934
- timeout: BASH_TIMEOUT_MS,
935
- shell: '/bin/bash',
936
- });
937
- const output = [stdout, stderr].filter(Boolean).join('\n');
938
- return {
939
- content: [{ type: 'text', text: output || '(no output)' }],
940
- details: { stdout, stderr },
941
- };
942
- }
943
- catch (err) {
944
- const e = err;
945
- const stdout = String(e.stdout ?? '');
946
- const stderr = String(e.stderr ?? '');
947
- const rawCode = e.code;
948
- const signal = e.signal;
949
- if (rawCode === 1 && !stderr.trim()) {
950
- return {
951
- content: [{ type: 'text', text: stdout || '(no output)' }],
952
- details: { stdout, stderr },
953
- };
954
- }
955
- const exitInfo = rawCode != null
956
- ? `exit ${String(rawCode)}`
957
- : signal
958
- ? `signal ${String(signal)}`
959
- : 'exit unknown';
960
- throw new Error(`Command failed: ${params.command} (${exitInfo})\nSTDOUT:\n${stdout}\nSTDERR:\n${stderr}`);
961
- }
962
- },
963
- };
964
- }
965
- function findActualString(fileContent, oldString) {
966
- if (fileContent.includes(oldString))
967
- return oldString;
968
- const normalized = oldString
969
- .replace(/[\u2018\u2019]/g, "'")
970
- .replace(/[\u201C\u201D]/g, '"')
971
- .replace(/\u2013/g, '-')
972
- .replace(/\u2014/g, '--');
973
- if (fileContent.includes(normalized))
974
- return normalized;
975
- return null;
976
- }
977
- const READ_SIZE_CAP_BYTES = 256 * 1024;
978
- const GLOB_ALWAYS_EXCLUDE = ['**/node_modules/**', '**/.git/**', '**/dist/**', '**/build/**'];
979
- function makeReadTool(readFileState, schemas, sessionId, emitter, workrailSessionId) {
980
- return {
981
- name: 'Read',
982
- description: 'Read the contents of a file at the given absolute path. ' +
983
- 'Content is returned in cat -n format: each line is prefixed with its 1-indexed line number and a tab character (e.g. "1\\tline one\\n2\\tline two"). ' +
984
- 'Use offset (0-indexed start line) and limit (max lines) to read a slice of a large file.',
985
- inputSchema: schemas['ReadParams'],
986
- label: 'Read',
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');
990
- const filePath = params.filePath;
991
- if (sessionId)
992
- emitter?.emit({ kind: 'tool_called', sessionId, toolName: 'Read', summary: filePath.slice(0, 80), ...withWorkrailSession(workrailSessionId) });
993
- const devPaths = ['/dev/stdin', '/dev/tty', '/dev/zero', '/dev/random', '/dev/full', '/dev/urandom'];
994
- if (devPaths.some(d => filePath === d)) {
995
- throw new Error(`Refusing to read device path: ${filePath}`);
996
- }
997
- const stat = await fs.stat(filePath);
998
- const offset = params.offset ?? 0;
999
- const limit = params.limit;
1000
- const isPaginated = params.offset !== undefined || params.limit !== undefined;
1001
- if (!isPaginated && stat.size > READ_SIZE_CAP_BYTES) {
1002
- throw new Error(`File is too large to read at once (${stat.size} bytes, cap is ${READ_SIZE_CAP_BYTES} bytes). ` +
1003
- `Use offset and limit parameters to read a specific range of lines.`);
1004
- }
1005
- const rawContent = await fs.readFile(filePath, 'utf8');
1006
- const allLines = rawContent.split('\n');
1007
- const isPartialView = offset !== 0 || limit != null;
1008
- const slicedLines = limit != null ? allLines.slice(offset, offset + limit) : allLines.slice(offset);
1009
- const startLine = offset;
1010
- const formatted = slicedLines.map((l, i) => `${startLine + i + 1}\t${l}`).join('\n');
1011
- readFileState.set(filePath, { content: rawContent, timestamp: stat.mtimeMs, isPartialView });
1012
- return {
1013
- content: [{ type: 'text', text: formatted }],
1014
- details: { filePath, totalLines: allLines.length, returnedLines: slicedLines.length, offset, isPartialView },
1015
- };
1016
- },
1017
- };
1018
- }
1019
- function makeWriteTool(readFileState, schemas, sessionId, emitter, workrailSessionId) {
1020
- return {
1021
- name: 'Write',
1022
- description: 'Write content to a file at the given absolute path. Creates parent directories if needed. ' +
1023
- 'For existing files: the file must have been read in this session and must not have changed on disk since then. ' +
1024
- 'For new files (path does not exist): no prior read is required.',
1025
- inputSchema: schemas['WriteParams'],
1026
- label: 'Write',
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');
1032
- const filePath = params.filePath;
1033
- if (sessionId)
1034
- emitter?.emit({ kind: 'tool_called', sessionId, toolName: 'Write', summary: filePath.slice(0, 80), ...withWorkrailSession(workrailSessionId) });
1035
- let existsOnDisk = false;
1036
- try {
1037
- await fs.access(filePath);
1038
- existsOnDisk = true;
1039
- }
1040
- catch {
1041
- }
1042
- if (existsOnDisk) {
1043
- const state = readFileState.get(filePath);
1044
- if (!state) {
1045
- throw new Error(`File has not been read in this session. Call Read first before writing to it: ${filePath}`);
1046
- }
1047
- const stat = await fs.stat(filePath);
1048
- if (stat.mtimeMs !== state.timestamp) {
1049
- throw new Error(`File has been modified since it was read. Re-read before writing: ${filePath}`);
1050
- }
1051
- }
1052
- await fs.mkdir(path.dirname(filePath), { recursive: true });
1053
- await fs.writeFile(filePath, params.content, 'utf8');
1054
- const newStat = await fs.stat(filePath);
1055
- readFileState.set(filePath, { content: params.content, timestamp: newStat.mtimeMs, isPartialView: false });
1056
- return {
1057
- content: [{ type: 'text', text: `Written ${params.content.length} bytes to ${filePath}` }],
1058
- details: { filePath, length: params.content.length },
1059
- };
1060
- },
1061
- };
1062
- }
1063
- function makeGlobTool(workspacePath, schemas, sessionId, emitter, workrailSessionId) {
1064
- return {
1065
- name: 'Glob',
1066
- description: 'Find files matching a glob pattern. Returns newline-separated relative file paths, sorted by modification time descending. ' +
1067
- 'node_modules, .git, dist, and build directories are always excluded. ' +
1068
- 'Results are capped at 100 files.',
1069
- inputSchema: schemas['GlobParams'],
1070
- label: 'Glob',
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');
1074
- const pattern = params.pattern;
1075
- const searchRoot = params.path ?? workspacePath;
1076
- if (sessionId)
1077
- emitter?.emit({ kind: 'tool_called', sessionId, toolName: 'Glob', summary: pattern.slice(0, 80), ...withWorkrailSession(workrailSessionId) });
1078
- const GLOB_LIMIT = 100;
1079
- let paths;
1080
- try {
1081
- paths = await (0, tinyglobby_1.glob)(pattern, {
1082
- cwd: searchRoot,
1083
- ignore: GLOB_ALWAYS_EXCLUDE,
1084
- absolute: false,
1085
- });
1086
- }
1087
- catch {
1088
- paths = [];
1089
- }
1090
- const withMtimes = await Promise.all(paths.map(async (p) => {
1091
- try {
1092
- const stat = await fs.stat(path.join(searchRoot, p));
1093
- return { p, mtime: stat.mtimeMs };
1094
- }
1095
- catch {
1096
- return { p, mtime: 0 };
1097
- }
1098
- }));
1099
- withMtimes.sort((a, b) => b.mtime - a.mtime);
1100
- const sorted = withMtimes.map(x => x.p);
1101
- const truncated = sorted.length > GLOB_LIMIT;
1102
- const result = sorted.slice(0, GLOB_LIMIT);
1103
- let text = result.join('\n');
1104
- if (truncated) {
1105
- text += '\n[Results truncated at 100 files]';
1106
- }
1107
- return {
1108
- content: [{ type: 'text', text: text || '(no matches)' }],
1109
- details: { pattern, searchRoot, matchCount: sorted.length, truncated },
1110
- };
1111
- },
1112
- };
1113
- }
1114
- function makeGrepTool(workspacePath, schemas, sessionId, emitter, workrailSessionId) {
1115
- return {
1116
- name: 'Grep',
1117
- description: 'Search file contents using ripgrep (rg). Fast regex search with optional context lines, file-type filtering, and case-insensitive mode. ' +
1118
- 'output_mode: "files_with_matches" (default) returns only file paths; "content" returns matching lines; "count" returns match counts per file. ' +
1119
- 'node_modules and .git are always excluded.',
1120
- inputSchema: schemas['GrepParams'],
1121
- label: 'Grep',
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');
1125
- const pattern = params.pattern;
1126
- const searchPath = params.path ?? workspacePath;
1127
- const outputMode = params.output_mode ?? 'files_with_matches';
1128
- const headLimit = params.head_limit ?? 250;
1129
- if (sessionId)
1130
- emitter?.emit({ kind: 'tool_called', sessionId, toolName: 'Grep', summary: pattern.slice(0, 80), ...withWorkrailSession(workrailSessionId) });
1131
- const args = [
1132
- '--hidden',
1133
- '--glob', '!node_modules',
1134
- '--glob', '!.git',
1135
- '--max-columns', '500',
1136
- ];
1137
- if (params['-i'])
1138
- args.push('-i');
1139
- if (params.glob) {
1140
- args.push('--glob', params.glob);
1141
- }
1142
- if (params.type) {
1143
- args.push('--type', params.type);
1144
- }
1145
- switch (outputMode) {
1146
- case 'files_with_matches':
1147
- args.push('--files-with-matches');
1148
- break;
1149
- case 'count':
1150
- args.push('--count');
1151
- break;
1152
- case 'content':
1153
- args.push('--vimgrep');
1154
- if (params.context != null) {
1155
- args.push('-C', String(params.context));
1156
- }
1157
- break;
1158
- }
1159
- args.push('--', pattern, searchPath);
1160
- let stdout;
1161
- try {
1162
- const result = await execFileAsync('rg', args, { cwd: workspacePath, maxBuffer: 10 * 1024 * 1024 });
1163
- stdout = result.stdout;
1164
- }
1165
- catch (err) {
1166
- const nodeErr = err;
1167
- if (nodeErr.code === 'ENOENT') {
1168
- throw new Error('ripgrep (rg) is not installed. Install it with: brew install ripgrep (macOS) or apt install ripgrep (Ubuntu/Debian).');
1169
- }
1170
- if (typeof nodeErr.code === 'number' && nodeErr.code === 1) {
1171
- return {
1172
- content: [{ type: 'text', text: '(no matches)' }],
1173
- details: { pattern, searchPath, outputMode },
1174
- };
1175
- }
1176
- throw new Error(`rg failed: ${nodeErr.message ?? String(err)}`);
1177
- }
1178
- const lines = stdout.split('\n').filter(l => l.length > 0);
1179
- const truncated = lines.length > headLimit;
1180
- let result = lines.slice(0, headLimit).join('\n');
1181
- if (truncated) {
1182
- result += `\n[Results truncated at ${headLimit} lines. Use a more specific pattern or increase head_limit.]`;
1183
- }
1184
- return {
1185
- content: [{ type: 'text', text: result || '(no matches)' }],
1186
- details: { pattern, searchPath, outputMode, lineCount: lines.length, truncated },
1187
- };
1188
- },
1189
- };
1190
- }
1191
- function makeEditTool(workspacePath, readFileState, schemas, sessionId, emitter, workrailSessionId) {
1192
- return {
1193
- name: 'Edit',
1194
- description: 'Perform an exact string replacement in a file. ' +
1195
- 'The file must have been read in this session via the Read tool. ' +
1196
- 'By default, old_string must appear exactly once; use replace_all=true to replace all occurrences. ' +
1197
- 'Do NOT include line-number prefixes (e.g. "1\\t") from Read output in old_string or new_string.',
1198
- inputSchema: schemas['EditParams'],
1199
- label: 'Edit',
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');
1207
- const rawFilePath = params.file_path;
1208
- const absoluteFilePath = path.isAbsolute(rawFilePath)
1209
- ? rawFilePath
1210
- : path.join(workspacePath, rawFilePath);
1211
- if (!absoluteFilePath.startsWith(workspacePath)) {
1212
- throw new Error(`Edit target is outside the workspace: ${rawFilePath}`);
1213
- }
1214
- const filePath = absoluteFilePath;
1215
- const oldString = params.old_string;
1216
- const newString = params.new_string;
1217
- const replaceAll = params.replace_all ?? false;
1218
- if (sessionId)
1219
- emitter?.emit({ kind: 'tool_called', sessionId, toolName: 'Edit', summary: filePath.slice(0, 80), ...withWorkrailSession(workrailSessionId) });
1220
- if (oldString === newString) {
1221
- throw new Error('old_string and new_string are identical. No edit needed.');
1222
- }
1223
- const state = readFileState.get(filePath);
1224
- if (!state) {
1225
- throw new Error(`File has not been read in this session. Call Read first before editing: ${filePath}`);
1226
- }
1227
- let stat;
1228
- try {
1229
- stat = await fs.stat(filePath);
1230
- }
1231
- catch {
1232
- throw new Error(`File not found: ${filePath}. It may have been deleted after it was read.`);
1233
- }
1234
- if (stat.mtimeMs !== state.timestamp) {
1235
- throw new Error(`File has been modified since it was read. Re-read before editing: ${filePath}`);
1236
- }
1237
- const currentContent = await fs.readFile(filePath, 'utf8');
1238
- const actualString = findActualString(currentContent, oldString);
1239
- if (actualString === null) {
1240
- throw new Error(`String to replace not found in file. Make sure old_string exactly matches the file content ` +
1241
- `(do not include line-number prefixes from Read output): ${filePath}`);
1242
- }
1243
- const occurrences = currentContent.split(actualString).length - 1;
1244
- if (!replaceAll && occurrences > 1) {
1245
- throw new Error(`old_string appears ${occurrences} times in the file. ` +
1246
- `Provide a more specific string that matches exactly once, or set replace_all=true to replace all occurrences.`);
1247
- }
1248
- const updatedContent = replaceAll
1249
- ? currentContent.split(actualString).join(newString)
1250
- : currentContent.replace(actualString, newString);
1251
- await fs.writeFile(filePath, updatedContent, 'utf8');
1252
- const newStat = await fs.stat(filePath);
1253
- readFileState.set(filePath, { content: updatedContent, timestamp: newStat.mtimeMs, isPartialView: false });
1254
- return {
1255
- content: [{ type: 'text', text: `The file ${filePath} has been updated successfully.` }],
1256
- details: { filePath, occurrencesReplaced: occurrences },
1257
- };
1258
- },
1259
- };
1260
- }
1261
- function makeSpawnAgentTool(sessionId, ctx, apiKey, thisWorkrailSessionId, currentDepth, maxDepth, runWorkflowFn, schemas, emitter, abortRegistry) {
1262
- return {
1263
- name: 'spawn_agent',
1264
- description: 'Spawn a child WorkRail session to handle a delegated sub-task. ' +
1265
- 'Blocks until the child session completes, then returns the child\'s outcome and notes. ' +
1266
- 'Use this when a step requires delegating a well-defined sub-task to a separate workflow. ' +
1267
- 'IMPORTANT: The parent session\'s time limit (maxSessionMinutes) keeps ticking while the child runs. ' +
1268
- 'Configure the parent with enough time to cover both its own work and the child\'s work. ' +
1269
- 'Per-trigger limits (maxOutputTokens, maxTurns, maxSessionMinutes) are NOT inherited by child sessions spawned via spawn_agent -- each child uses its own trigger\'s agentConfig. ' +
1270
- 'Returns: { childSessionId, outcome: "success"|"error"|"timeout", notes: string, artifacts?: readonly unknown[] }. ' +
1271
- 'On success, artifacts contains the child session\'s final step artifacts if any were produced. ' +
1272
- 'Check outcome before using notes -- on error/timeout, notes contains the error message.',
1273
- inputSchema: schemas['SpawnAgentParams'],
1274
- label: 'Spawn Agent',
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');
1282
- console.log(`[WorkflowRunner] Tool: spawn_agent sessionId=${sessionId} workflowId=${String(params.workflowId)} depth=${currentDepth}/${maxDepth}`);
1283
- emitter?.emit({ kind: 'tool_called', sessionId, toolName: 'spawn_agent', summary: `${String(params.workflowId)} depth=${currentDepth}`, ...withWorkrailSession(thisWorkrailSessionId) });
1284
- if (currentDepth >= maxDepth) {
1285
- const limitResult = {
1286
- childSessionId: null,
1287
- outcome: 'error',
1288
- notes: `Max spawn depth exceeded (currentDepth=${currentDepth}, maxDepth=${maxDepth}). ` +
1289
- `Cannot spawn a child session from this depth. ` +
1290
- `Increase agentConfig.maxSubagentDepth if deeper delegation is intentional.`,
1291
- };
1292
- return {
1293
- content: [{ type: 'text', text: JSON.stringify(limitResult) }],
1294
- details: limitResult,
1295
- };
1296
- }
1297
- const startInput = {
1298
- workflowId: String(params.workflowId),
1299
- workspacePath: String(params.workspacePath),
1300
- goal: String(params.goal),
1301
- };
1302
- const startResult = await (0, start_js_1.executeStartWorkflow)(startInput, ctx, { is_autonomous: 'true', workspacePath: String(params.workspacePath), parentSessionId: thisWorkrailSessionId });
1303
- if (startResult.isErr()) {
1304
- const errResult = {
1305
- childSessionId: null,
1306
- outcome: 'error',
1307
- notes: `Failed to start child workflow: ${startResult.error.kind} -- ${JSON.stringify(startResult.error)}`,
1308
- };
1309
- return {
1310
- content: [{ type: 'text', text: JSON.stringify(errResult) }],
1311
- details: errResult,
1312
- };
1313
- }
1314
- let childSessionId = null;
1315
- const childContinueToken = startResult.value.response.continueToken ?? '';
1316
- if (childContinueToken) {
1317
- const decoded = await (0, v2_token_ops_js_1.parseContinueTokenOrFail)(childContinueToken, ctx.v2.tokenCodecPorts, ctx.v2.tokenAliasStore);
1318
- if (decoded.isOk()) {
1319
- childSessionId = decoded.value.sessionId;
1320
- }
1321
- else {
1322
- console.warn(`[WorkflowRunner] spawn_agent: could not decode childSessionId from continueToken -- ` +
1323
- `childSessionId will be null in result. Reason: ${decoded.error.message}`);
1324
- }
1325
- }
1326
- const childResult = await runWorkflowFn({
1327
- workflowId: String(params.workflowId),
1328
- goal: String(params.goal),
1329
- workspacePath: String(params.workspacePath),
1330
- context: params.context,
1331
- spawnDepth: currentDepth + 1,
1332
- parentSessionId: thisWorkrailSessionId,
1333
- _preAllocatedStartResponse: startResult.value.response,
1334
- }, ctx, apiKey, undefined, emitter, undefined, abortRegistry);
1335
- let resultObj;
1336
- if (childResult._tag === 'success') {
1337
- resultObj = {
1338
- childSessionId,
1339
- outcome: 'success',
1340
- notes: childResult.lastStepNotes ?? '(no notes from child session)',
1341
- ...(childResult.lastStepArtifacts !== undefined ? { artifacts: childResult.lastStepArtifacts } : {}),
1342
- };
1343
- }
1344
- else if (childResult._tag === 'error') {
1345
- resultObj = {
1346
- childSessionId,
1347
- outcome: 'error',
1348
- notes: childResult.message,
1349
- };
1350
- }
1351
- else if (childResult._tag === 'timeout') {
1352
- resultObj = {
1353
- childSessionId,
1354
- outcome: 'timeout',
1355
- notes: childResult.message,
1356
- };
1357
- }
1358
- else if (childResult._tag === 'stuck') {
1359
- resultObj = {
1360
- childSessionId,
1361
- outcome: 'stuck',
1362
- notes: childResult.message,
1363
- ...(childResult.issueSummaries !== undefined
1364
- ? { issueSummaries: childResult.issueSummaries }
1365
- : {}),
1366
- };
1367
- }
1368
- else {
1369
- (0, assert_never_js_1.assertNever)(childResult);
1370
- }
1371
- console.log(`[WorkflowRunner] spawn_agent completed: sessionId=${sessionId} childSessionId=${childSessionId ?? 'null'} outcome=${resultObj.outcome}`);
1372
- emitter?.emit({ kind: 'tool_called', sessionId, toolName: 'spawn_agent_complete', summary: `outcome=${resultObj.outcome} child=${childSessionId ?? 'null'}`, ...withWorkrailSession(thisWorkrailSessionId) });
1373
- return {
1374
- content: [{ type: 'text', text: JSON.stringify(resultObj) }],
1375
- details: resultObj,
1376
- };
1377
- },
1378
- };
1379
- }
1380
699
  async function writeStuckOutboxEntry(opts) {
1381
700
  try {
1382
701
  const outboxPath = path.join(os.homedir(), '.workrail', 'outbox.jsonl');
@@ -1402,172 +721,6 @@ async function writeStuckOutboxEntry(opts) {
1402
721
  `${err instanceof Error ? err.message : String(err)}`);
1403
722
  }
1404
723
  }
1405
- async function appendIssueAsync(issuesDir, sessionId, record) {
1406
- await fs.mkdir(issuesDir, { recursive: true });
1407
- const filePath = path.join(issuesDir, `${sessionId}.jsonl`);
1408
- const line = JSON.stringify({ ...record, ts: Date.now() }) + '\n';
1409
- await fs.appendFile(filePath, line, 'utf8');
1410
- }
1411
- function makeReportIssueTool(sessionId, emitter, workrailSessionId, issuesDirOverride, onIssueSummary) {
1412
- const issuesDir = issuesDirOverride ?? path.join(os.homedir(), '.workrail', 'issues');
1413
- return {
1414
- name: 'report_issue',
1415
- description: "Record a structured issue, error, or unexpected behavior. Call this AND continue_workflow (unless fatal). " +
1416
- "Does not stop the session -- it creates a record for the auto-fix coordinator.",
1417
- inputSchema: {
1418
- type: 'object',
1419
- properties: {
1420
- kind: {
1421
- type: 'string',
1422
- enum: ['tool_failure', 'blocked', 'unexpected_behavior', 'needs_human', 'self_correction'],
1423
- description: 'Category of issue being reported.',
1424
- },
1425
- severity: {
1426
- type: 'string',
1427
- enum: ['info', 'warn', 'error', 'fatal'],
1428
- description: 'Severity level. Fatal means the session cannot continue productively.',
1429
- },
1430
- summary: {
1431
- type: 'string',
1432
- description: 'One-line summary of the issue. Max 200 chars.',
1433
- maxLength: 200,
1434
- },
1435
- context: {
1436
- type: 'string',
1437
- description: 'What you were trying to do when this issue occurred.',
1438
- },
1439
- toolName: {
1440
- type: 'string',
1441
- description: 'Name of the tool that failed or behaved unexpectedly, if applicable.',
1442
- },
1443
- command: {
1444
- type: 'string',
1445
- description: 'The shell command or expression that caused the issue, if applicable.',
1446
- },
1447
- suggestedFix: {
1448
- type: 'string',
1449
- description: 'A suggested fix or recovery action for the auto-fix coordinator.',
1450
- },
1451
- continueToken: {
1452
- type: 'string',
1453
- description: 'The current continueToken, so the coordinator can resume this session.',
1454
- },
1455
- },
1456
- required: ['kind', 'severity', 'summary'],
1457
- additionalProperties: false,
1458
- },
1459
- label: 'report_issue',
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');
1467
- const record = {
1468
- sessionId,
1469
- kind: params.kind,
1470
- severity: params.severity,
1471
- summary: String(params.summary ?? '').slice(0, 200),
1472
- ...(params.context !== undefined && { context: String(params.context) }),
1473
- ...(params.toolName !== undefined && { toolName: String(params.toolName) }),
1474
- ...(params.command !== undefined && { command: String(params.command) }),
1475
- ...(params.suggestedFix !== undefined && { suggestedFix: String(params.suggestedFix) }),
1476
- ...(params.continueToken !== undefined && { continueToken: String(params.continueToken) }),
1477
- };
1478
- void appendIssueAsync(issuesDir, sessionId, record).catch(() => {
1479
- });
1480
- emitter?.emit({
1481
- kind: 'issue_reported',
1482
- sessionId,
1483
- issueKind: record.kind,
1484
- severity: record.severity,
1485
- summary: record.summary,
1486
- ...(record.continueToken !== undefined && { continueToken: record.continueToken }),
1487
- ...(workrailSessionId != null ? { workrailSessionId } : {}),
1488
- });
1489
- onIssueSummary?.(record.summary);
1490
- const isFatal = record.severity === 'fatal';
1491
- const message = isFatal
1492
- ? `FATAL issue recorded. Call continue_workflow with notes explaining the blocker, then the session will end.`
1493
- : `Issue recorded (severity=${record.severity}). Continue with your work unless this is fatal.`;
1494
- return {
1495
- content: [{ type: 'text', text: message }],
1496
- details: { sessionId, kind: record.kind, severity: record.severity },
1497
- };
1498
- },
1499
- };
1500
- }
1501
- exports.DAEMON_SIGNALS_DIR = path.join(os.homedir(), '.workrail', 'signals');
1502
- async function appendSignalAsync(signalsDir, sessionId, record) {
1503
- await fs.mkdir(signalsDir, { recursive: true });
1504
- const filePath = path.join(signalsDir, `${sessionId}.jsonl`);
1505
- const line = JSON.stringify({ ...record, ts: Date.now() }) + '\n';
1506
- await fs.appendFile(filePath, line, 'utf8');
1507
- }
1508
- function makeSignalCoordinatorTool(sessionId, emitter, workrailSessionId, signalsDirOverride) {
1509
- const signalsDir = signalsDirOverride ?? exports.DAEMON_SIGNALS_DIR;
1510
- return {
1511
- name: 'signal_coordinator',
1512
- description: 'Emit a structured mid-session signal to the coordinator WITHOUT advancing the workflow step. ' +
1513
- 'Use this to surface progress updates, intermediate findings, data requests, ' +
1514
- 'approval requests, or blocking conditions while the session continues. ' +
1515
- 'Always returns immediately -- fire-and-observe, never blocks. ' +
1516
- 'Signal kinds: "progress" (heartbeat, no data needed), "finding" (intermediate result), ' +
1517
- '"data_needed" (request external data), "approval_needed" (request coordinator approval), ' +
1518
- '"blocked" (cannot continue without coordinator intervention).',
1519
- inputSchema: {
1520
- type: 'object',
1521
- properties: {
1522
- signalKind: {
1523
- type: 'string',
1524
- enum: ['progress', 'finding', 'data_needed', 'approval_needed', 'blocked'],
1525
- description: 'The kind of signal to emit.',
1526
- },
1527
- payload: {
1528
- type: 'object',
1529
- additionalProperties: true,
1530
- description: 'Structured data accompanying the signal. Pass {} for progress signals.',
1531
- },
1532
- },
1533
- required: ['signalKind', 'payload'],
1534
- additionalProperties: false,
1535
- },
1536
- label: 'signal_coordinator',
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');
1540
- const signalId = 'sig_' + (0, node_crypto_1.randomUUID)().replace(/-/g, '').slice(0, 8);
1541
- const signalKind = String(params.signalKind ?? 'progress');
1542
- const payload = (typeof params.payload === 'object' && params.payload !== null && !Array.isArray(params.payload))
1543
- ? params.payload
1544
- : {};
1545
- console.log(`[WorkflowRunner] Tool: signal_coordinator sessionId=${sessionId} signalKind=${signalKind} signalId=${signalId}`);
1546
- const record = {
1547
- signalId,
1548
- sessionId,
1549
- ...(workrailSessionId != null ? { workrailSessionId } : {}),
1550
- signalKind,
1551
- payload,
1552
- };
1553
- void appendSignalAsync(signalsDir, sessionId, record).catch(() => {
1554
- });
1555
- emitter?.emit({
1556
- kind: 'signal_emitted',
1557
- sessionId,
1558
- signalKind,
1559
- signalId,
1560
- payload,
1561
- ...(workrailSessionId != null ? { workrailSessionId } : {}),
1562
- });
1563
- const result = { status: 'recorded', signalId };
1564
- return {
1565
- content: [{ type: 'text', text: JSON.stringify(result) }],
1566
- details: result,
1567
- };
1568
- },
1569
- };
1570
- }
1571
724
  const BASE_SYSTEM_PROMPT = `\
1572
725
  You are WorkRail Auto, an autonomous agent that executes workflows step by step. You are running unattended -- there is no user watching. Your entire job is to faithfully complete the current workflow.
1573
726
 
@@ -1787,7 +940,7 @@ async function finalizeSession(result, ctx) {
1787
940
  workflowId: ctx.workflowId,
1788
941
  outcome,
1789
942
  detail,
1790
- ...withWorkrailSession(ctx.workrailSessionId),
943
+ ...(0, _shared_js_1.withWorkrailSession)(ctx.workrailSessionId),
1791
944
  });
1792
945
  if (ctx.workrailSessionId !== null) {
1793
946
  ctx.daemonRegistry?.unregister(ctx.workrailSessionId, result._tag === 'success' || result._tag === 'delivery_failed' ? 'completed' : 'failed');
@@ -1807,20 +960,22 @@ async function finalizeSession(result, ctx) {
1807
960
  await fs.unlink(ctx.conversationPath).catch(() => { });
1808
961
  }
1809
962
  }
1810
- function buildSessionContext(trigger, inputs) {
1811
- const sessionState = buildSessionRecap(inputs.sessionNotes);
1812
- const systemPrompt = buildSystemPrompt(trigger, sessionState, inputs.soulContent, inputs.workspaceContext);
963
+ function buildSessionContext(trigger, context, firstStepPrompt) {
964
+ const workspaceContext = context.workspaceRules[0]?.content ?? null;
965
+ const sessionNotes = context.sessionHistory.map((n) => n.content);
966
+ const sessionState = buildSessionRecap(sessionNotes);
967
+ const systemPrompt = buildSystemPrompt(trigger, sessionState, context.soulContent, workspaceContext);
1813
968
  const contextJson = trigger.context
1814
969
  ? `\n\nTrigger context:\n\`\`\`json\n${JSON.stringify(trigger.context, null, 2)}\n\`\`\``
1815
970
  : '';
1816
- const initialPrompt = inputs.firstStepPrompt +
971
+ const initialPrompt = firstStepPrompt +
1817
972
  contextJson +
1818
973
  '\n\nComplete all step work, then call complete_step with your notes to advance.';
1819
974
  const sessionTimeoutMs = (trigger.agentConfig?.maxSessionMinutes ?? exports.DEFAULT_SESSION_TIMEOUT_MINUTES) * 60 * 1000;
1820
975
  const maxTurns = trigger.agentConfig?.maxTurns ?? exports.DEFAULT_MAX_TURNS;
1821
976
  return { systemPrompt, initialPrompt, sessionTimeoutMs, maxTurns };
1822
977
  }
1823
- async function buildPreAgentSession(trigger, ctx, apiKey, sessionId, startMs, statsDir, sessionsDir, emitter, daemonRegistry, steerRegistry) {
978
+ async function buildPreAgentSession(trigger, ctx, apiKey, sessionId, startMs, statsDir, sessionsDir, emitter, daemonRegistry, activeSessionSet, source) {
1824
979
  let agentClient;
1825
980
  let modelId;
1826
981
  try {
@@ -1844,9 +999,17 @@ async function buildPreAgentSession(trigger, ctx, apiKey, sessionId, startMs, st
1844
999
  return { kind: 'complete', result: { _tag: 'error', workflowId: trigger.workflowId, message, stopReason: 'error' } };
1845
1000
  }
1846
1001
  const state = createSessionState('');
1847
- let firstStep;
1848
- if (trigger._preAllocatedStartResponse !== undefined) {
1849
- firstStep = trigger._preAllocatedStartResponse;
1002
+ let continueToken;
1003
+ let checkpointToken;
1004
+ let firstStepPrompt;
1005
+ let isComplete;
1006
+ const effectiveSource = source ?? { kind: 'allocate', trigger };
1007
+ if (effectiveSource.kind === 'pre_allocated') {
1008
+ const s = effectiveSource.session;
1009
+ continueToken = s.continueToken;
1010
+ checkpointToken = s.checkpointToken ?? null;
1011
+ firstStepPrompt = s.firstStepPrompt;
1012
+ isComplete = s.isComplete;
1850
1013
  }
1851
1014
  else {
1852
1015
  const startResult = await (0, start_js_1.executeStartWorkflow)({ workflowId: trigger.workflowId, workspacePath: trigger.workspacePath, goal: trigger.goal }, ctx, { is_autonomous: 'true', workspacePath: trigger.workspacePath });
@@ -1862,10 +1025,12 @@ async function buildPreAgentSession(trigger, ctx, apiKey, sessionId, startMs, st
1862
1025
  },
1863
1026
  };
1864
1027
  }
1865
- firstStep = startResult.value.response;
1028
+ const r = startResult.value.response;
1029
+ continueToken = r.continueToken ?? '';
1030
+ checkpointToken = r.checkpointToken ?? null;
1031
+ firstStepPrompt = r.pending?.prompt ?? '';
1032
+ isComplete = r.isComplete;
1866
1033
  }
1867
- const continueToken = firstStep.continueToken ?? '';
1868
- const checkpointToken = firstStep.checkpointToken ?? null;
1869
1034
  state.currentContinueToken = continueToken;
1870
1035
  if (continueToken) {
1871
1036
  const decoded = await (0, v2_token_ops_js_1.parseContinueTokenOrFail)(continueToken, ctx.v2.tokenCodecPorts, ctx.v2.tokenAliasStore);
@@ -1877,7 +1042,7 @@ async function buildPreAgentSession(trigger, ctx, apiKey, sessionId, startMs, st
1877
1042
  }
1878
1043
  }
1879
1044
  if (continueToken) {
1880
- const persistResult = await persistTokens(sessionId, continueToken, checkpointToken, undefined, {
1045
+ const persistResult = await (0, _shared_js_1.persistTokens)(sessionId, continueToken, checkpointToken, undefined, {
1881
1046
  workflowId: trigger.workflowId,
1882
1047
  goal: trigger.goal,
1883
1048
  workspacePath: trigger.workspacePath,
@@ -1912,7 +1077,7 @@ async function buildPreAgentSession(trigger, ctx, apiKey, sessionId, startMs, st
1912
1077
  '-b', `${branchPrefix}${sessionId}`,
1913
1078
  `origin/${baseBranch}`,
1914
1079
  ]);
1915
- const worktreePersistResult = await persistTokens(sessionId, continueToken ?? state.currentContinueToken, checkpointToken, sessionWorktreePath, { workflowId: trigger.workflowId, goal: trigger.goal, workspacePath: trigger.workspacePath });
1080
+ const worktreePersistResult = await (0, _shared_js_1.persistTokens)(sessionId, continueToken ?? state.currentContinueToken, checkpointToken, sessionWorktreePath, { workflowId: trigger.workflowId, goal: trigger.goal, workspacePath: trigger.workspacePath });
1916
1081
  if (worktreePersistResult.kind === 'err') {
1917
1082
  console.error(`[WorkflowRunner] Worktree sidecar persist failed: ${worktreePersistResult.error.code} -- ${worktreePersistResult.error.message}`);
1918
1083
  try {
@@ -1942,19 +1107,20 @@ async function buildPreAgentSession(trigger, ctx, apiKey, sessionId, startMs, st
1942
1107
  };
1943
1108
  }
1944
1109
  }
1110
+ let handle;
1945
1111
  if (state.workrailSessionId !== null) {
1946
1112
  daemonRegistry?.register(state.workrailSessionId, trigger.workflowId);
1947
- steerRegistry?.set(state.workrailSessionId, (text) => { state.pendingSteerParts.push(text); });
1113
+ handle = activeSessionSet?.register(state.workrailSessionId, (text) => { state.pendingSteerParts.push(text); });
1948
1114
  }
1949
- if (firstStep.isComplete) {
1115
+ if (isComplete) {
1950
1116
  const lifecycle = sidecardLifecycleFor('success', trigger.branchStrategy);
1951
1117
  if (lifecycle.kind === 'delete_now') {
1952
1118
  await fs.unlink(path.join(sessionsDir, `${sessionId}.json`)).catch(() => { });
1953
1119
  }
1954
- emitter?.emit({ kind: 'session_completed', sessionId, workflowId: trigger.workflowId, outcome: 'success', detail: 'stop', ...withWorkrailSession(state.workrailSessionId) });
1120
+ emitter?.emit({ kind: 'session_completed', sessionId, workflowId: trigger.workflowId, outcome: 'success', detail: 'stop', ...(0, _shared_js_1.withWorkrailSession)(state.workrailSessionId) });
1955
1121
  if (state.workrailSessionId !== null) {
1956
1122
  daemonRegistry?.unregister(state.workrailSessionId, 'completed');
1957
- steerRegistry?.delete(state.workrailSessionId);
1123
+ handle?.dispose();
1958
1124
  }
1959
1125
  writeExecutionStats(statsDir, sessionId, trigger.workflowId, startMs, 'success', 0);
1960
1126
  return {
@@ -1978,7 +1144,7 @@ async function buildPreAgentSession(trigger, ctx, apiKey, sessionId, startMs, st
1978
1144
  checkpointToken,
1979
1145
  sessionWorkspacePath,
1980
1146
  sessionWorktreePath,
1981
- firstStep,
1147
+ firstStepPrompt,
1982
1148
  state,
1983
1149
  spawnCurrentDepth: trigger.spawnDepth ?? 0,
1984
1150
  spawnMaxDepth: trigger.agentConfig?.maxSubagentDepth ?? 3,
@@ -1986,28 +1152,32 @@ async function buildPreAgentSession(trigger, ctx, apiKey, sessionId, startMs, st
1986
1152
  agentClient,
1987
1153
  modelId,
1988
1154
  startMs,
1155
+ ...(handle !== undefined ? { handle } : {}),
1989
1156
  },
1990
1157
  };
1991
1158
  }
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;
1159
+ function constructTools(session, ctx, apiKey, schemas, scope) {
1160
+ const { state, sessionWorkspacePath, spawnCurrentDepth, spawnMaxDepth } = session;
1161
+ const { fileTracker, onAdvance, onComplete, emitter, activeSessionSet, maxIssueSummaries } = scope;
1162
+ const sid = scope.sessionId;
1163
+ const workrailSid = scope.workrailSessionId;
1164
+ const readFileStateMap = fileTracker.toMap();
1995
1165
  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) => {
1166
+ (0, continue_workflow_js_1.makeCompleteStepTool)(sid, ctx, () => state.currentContinueToken, onAdvance, onComplete, (t) => { state.currentContinueToken = t; }, schemas, index_js_1.executeContinueWorkflow, emitter, workrailSid),
1167
+ (0, continue_workflow_js_1.makeContinueWorkflowTool)(sid, ctx, onAdvance, onComplete, schemas, index_js_1.executeContinueWorkflow, emitter, workrailSid),
1168
+ (0, bash_js_1.makeBashTool)(sessionWorkspacePath, schemas, sid, emitter, workrailSid),
1169
+ (0, file_tools_js_1.makeReadTool)(readFileStateMap, schemas, sid, emitter, workrailSid),
1170
+ (0, file_tools_js_1.makeWriteTool)(readFileStateMap, schemas, sid, emitter, workrailSid),
1171
+ (0, glob_grep_js_1.makeGlobTool)(sessionWorkspacePath, schemas, sid, emitter, workrailSid),
1172
+ (0, glob_grep_js_1.makeGrepTool)(sessionWorkspacePath, schemas, sid, emitter, workrailSid),
1173
+ (0, file_tools_js_1.makeEditTool)(sessionWorkspacePath, readFileStateMap, schemas, sid, emitter, workrailSid),
1174
+ (0, report_issue_js_1.makeReportIssueTool)(sid, emitter, workrailSid, undefined, (summary) => {
2005
1175
  if (state.issueSummaries.length < maxIssueSummaries) {
2006
1176
  state.issueSummaries.push(summary);
2007
1177
  }
2008
1178
  }),
2009
- makeSpawnAgentTool(sid, ctx, apiKey, workrailSid ?? '', spawnCurrentDepth, spawnMaxDepth, runWorkflow, schemas, emitter, abortRegistry),
2010
- makeSignalCoordinatorTool(sid, emitter, workrailSid),
1179
+ (0, spawn_agent_js_1.makeSpawnAgentTool)(sid, ctx, apiKey, workrailSid ?? '', spawnCurrentDepth, spawnMaxDepth, runWorkflow, schemas, emitter, activeSessionSet),
1180
+ (0, signal_coordinator_js_1.makeSignalCoordinatorTool)(sid, emitter, workrailSid),
2011
1181
  ];
2012
1182
  }
2013
1183
  function buildTurnEndSubscriber(ctx) {
@@ -2017,7 +1187,7 @@ function buildTurnEndSubscriber(ctx) {
2017
1187
  for (const toolResult of event.toolResults) {
2018
1188
  if (toolResult.isError) {
2019
1189
  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) });
1190
+ ctx.emitter?.emit({ kind: 'tool_error', sessionId: ctx.sessionId, toolName: toolResult.toolName, error: errorText.slice(0, 200), ...(0, _shared_js_1.withWorkrailSession)(ctx.state.workrailSessionId) });
2021
1191
  }
2022
1192
  }
2023
1193
  ctx.state.turnCount++;
@@ -2025,12 +1195,12 @@ function buildTurnEndSubscriber(ctx) {
2025
1195
  if (signal !== null) {
2026
1196
  if (signal.kind === 'max_turns_exceeded') {
2027
1197
  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) });
1198
+ ctx.emitter?.emit({ kind: 'agent_stuck', sessionId: ctx.sessionId, reason: 'timeout_imminent', detail: 'Max-turn limit reached', ...(0, _shared_js_1.withWorkrailSession)(ctx.state.workrailSessionId) });
2029
1199
  ctx.agent.abort();
2030
1200
  return;
2031
1201
  }
2032
1202
  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) });
1203
+ 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, ...(0, _shared_js_1.withWorkrailSession)(ctx.state.workrailSessionId) });
2034
1204
  void writeStuckOutboxEntry({ workflowId: ctx.workflowId, reason: 'repeated_tool_call', ...(ctx.state.issueSummaries.length > 0 ? { issueSummaries: [...ctx.state.issueSummaries] } : {}) });
2035
1205
  if (ctx.stuckConfig.stuckAbortPolicy !== 'notify_only' && ctx.state.stuckReason === null && ctx.state.timeoutReason === null) {
2036
1206
  ctx.state.stuckReason = 'repeated_tool_call';
@@ -2039,7 +1209,7 @@ function buildTurnEndSubscriber(ctx) {
2039
1209
  }
2040
1210
  }
2041
1211
  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) });
1212
+ ctx.emitter?.emit({ kind: 'agent_stuck', sessionId: ctx.sessionId, reason: 'no_progress', detail: `${signal.turnCount} turns used, 0 step advances (${signal.maxTurns} turn limit)`, ...(0, _shared_js_1.withWorkrailSession)(ctx.state.workrailSessionId) });
2043
1213
  if (ctx.stuckConfig.noProgressAbortEnabled) {
2044
1214
  void writeStuckOutboxEntry({ workflowId: ctx.workflowId, reason: 'no_progress', ...(ctx.state.issueSummaries.length > 0 ? { issueSummaries: [...ctx.state.issueSummaries] } : {}) });
2045
1215
  if (ctx.stuckConfig.stuckAbortPolicy !== 'notify_only' && ctx.state.stuckReason === null && ctx.state.timeoutReason === null) {
@@ -2050,42 +1220,35 @@ function buildTurnEndSubscriber(ctx) {
2050
1220
  }
2051
1221
  }
2052
1222
  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) });
1223
+ ctx.emitter?.emit({ kind: 'agent_stuck', sessionId: ctx.sessionId, reason: 'timeout_imminent', detail: `${signal.timeoutReason === 'wall_clock' ? 'Wall-clock timeout' : 'Max-turn limit'} reached`, ...(0, _shared_js_1.withWorkrailSession)(ctx.state.workrailSessionId) });
2054
1224
  }
2055
1225
  else {
2056
1226
  (0, assert_never_js_1.assertNever)(signal);
2057
1227
  }
2058
1228
  }
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
- }
1229
+ (0, conversation_flusher_js_1.flushConversation)(ctx.agent.state.messages, ctx.lastFlushedRef, ctx.conversationPath, appendConversationMessages);
1230
+ (0, step_injector_js_1.injectPendingSteps)(ctx.state, ctx.agent);
2068
1231
  };
2069
1232
  }
2070
1233
  function buildAgentCallbacks(sessionId, state, modelId, emitter, stuckRepeatThreshold) {
2071
1234
  return {
2072
1235
  onLlmTurnStarted: ({ messageCount }) => {
2073
- emitter?.emit({ kind: 'llm_turn_started', sessionId, messageCount, modelId, ...withWorkrailSession(state.workrailSessionId) });
1236
+ emitter?.emit({ kind: 'llm_turn_started', sessionId, messageCount, modelId, ...(0, _shared_js_1.withWorkrailSession)(state.workrailSessionId) });
2074
1237
  },
2075
1238
  onLlmTurnCompleted: ({ stopReason, outputTokens, inputTokens, toolNamesRequested }) => {
2076
- emitter?.emit({ kind: 'llm_turn_completed', sessionId, stopReason, outputTokens, inputTokens, toolNamesRequested, ...withWorkrailSession(state.workrailSessionId) });
1239
+ emitter?.emit({ kind: 'llm_turn_completed', sessionId, stopReason, outputTokens, inputTokens, toolNamesRequested, ...(0, _shared_js_1.withWorkrailSession)(state.workrailSessionId) });
2077
1240
  },
2078
1241
  onToolCallStarted: ({ toolName, argsSummary }) => {
2079
- emitter?.emit({ kind: 'tool_call_started', sessionId, toolName, argsSummary, ...withWorkrailSession(state.workrailSessionId) });
1242
+ emitter?.emit({ kind: 'tool_call_started', sessionId, toolName, argsSummary, ...(0, _shared_js_1.withWorkrailSession)(state.workrailSessionId) });
2080
1243
  state.lastNToolCalls.push({ toolName, argsSummary });
2081
1244
  if (state.lastNToolCalls.length > stuckRepeatThreshold)
2082
1245
  state.lastNToolCalls.shift();
2083
1246
  },
2084
1247
  onToolCallCompleted: ({ toolName, durationMs, resultSummary }) => {
2085
- emitter?.emit({ kind: 'tool_call_completed', sessionId, toolName, durationMs, resultSummary, ...withWorkrailSession(state.workrailSessionId) });
1248
+ emitter?.emit({ kind: 'tool_call_completed', sessionId, toolName, durationMs, resultSummary, ...(0, _shared_js_1.withWorkrailSession)(state.workrailSessionId) });
2086
1249
  },
2087
1250
  onToolCallFailed: ({ toolName, durationMs, errorMessage }) => {
2088
- emitter?.emit({ kind: 'tool_call_failed', sessionId, toolName, durationMs, errorMessage, ...withWorkrailSession(state.workrailSessionId) });
1251
+ emitter?.emit({ kind: 'tool_call_failed', sessionId, toolName, durationMs, errorMessage, ...(0, _shared_js_1.withWorkrailSession)(state.workrailSessionId) });
2089
1252
  },
2090
1253
  };
2091
1254
  }
@@ -2144,25 +1307,10 @@ function buildSessionResult(state, stopReason, errorMessage, trigger, sessionId,
2144
1307
  ...(trigger.botIdentity !== undefined ? { botIdentity: trigger.botIdentity } : {}),
2145
1308
  };
2146
1309
  }
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;
1310
+ async function buildAgentReadySession(preAgentSession, trigger, ctx, apiKey, sessionId, emitter, daemonRegistry, activeSessionSet) {
1311
+ const { state, firstStepPrompt, sessionWorkspacePath, sessionWorktreePath, agentClient, modelId } = preAgentSession;
1312
+ const startContinueToken = preAgentSession.continueToken;
1313
+ const handle = preAgentSession.handle;
2166
1314
  const MAX_ISSUE_SUMMARIES = 10;
2167
1315
  const STUCK_REPEAT_THRESHOLD = 3;
2168
1316
  const onAdvance = (stepText, continueToken) => {
@@ -2171,30 +1319,30 @@ async function runWorkflow(trigger, ctx, apiKey, daemonRegistry, emitter, steerR
2171
1319
  state.currentContinueToken = continueToken;
2172
1320
  if (state.workrailSessionId !== null)
2173
1321
  daemonRegistry?.heartbeat(state.workrailSessionId);
2174
- emitter?.emit({ kind: 'step_advanced', sessionId, ...withWorkrailSession(state.workrailSessionId) });
1322
+ emitter?.emit({ kind: 'step_advanced', sessionId, ...(0, _shared_js_1.withWorkrailSession)(state.workrailSessionId) });
2175
1323
  };
2176
1324
  const onComplete = (notes, artifacts) => {
2177
1325
  state.isComplete = true;
2178
1326
  state.lastStepNotes = notes;
2179
1327
  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
1328
  };
2185
1329
  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
- });
1330
+ const scope = {
1331
+ fileTracker: new session_scope_js_1.DefaultFileStateTracker(preAgentSession.readFileState),
1332
+ onAdvance,
1333
+ onComplete,
1334
+ workrailSessionId: state.workrailSessionId,
1335
+ emitter,
1336
+ sessionId,
1337
+ workflowId: trigger.workflowId,
1338
+ activeSessionSet,
1339
+ maxIssueSummaries: MAX_ISSUE_SUMMARIES,
1340
+ };
1341
+ const tools = constructTools(preAgentSession, ctx, apiKey, schemas, scope);
1342
+ const contextLoader = new context_loader_js_1.DefaultContextLoader(loadDaemonSoul, loadWorkspaceContext, loadSessionNotes, ctx);
1343
+ const baseCtx = await contextLoader.loadBase(trigger);
1344
+ const contextBundle = await contextLoader.loadSession(startContinueToken, baseCtx);
1345
+ const sessionCtx = buildSessionContext(trigger, contextBundle, firstStepPrompt || 'No step content available');
2198
1346
  const agentCallbacks = buildAgentCallbacks(sessionId, state, modelId, emitter, STUCK_REPEAT_THRESHOLD);
2199
1347
  const agent = new agent_loop_js_1.AgentLoop({
2200
1348
  systemPrompt: sessionCtx.systemPrompt,
@@ -2207,17 +1355,33 @@ async function runWorkflow(trigger, ctx, apiKey, daemonRegistry, emitter, steerR
2207
1355
  ? { maxTokens: trigger.agentConfig.maxOutputTokens }
2208
1356
  : {}),
2209
1357
  });
2210
- if (state.workrailSessionId !== null) {
2211
- abortRegistry?.set(state.workrailSessionId, () => { agent.abort(); });
2212
- }
1358
+ handle?.setAgent(agent);
1359
+ return {
1360
+ preAgentSession,
1361
+ contextBundle,
1362
+ scope,
1363
+ tools,
1364
+ sessionCtx,
1365
+ handle,
1366
+ sessionId,
1367
+ workflowId: trigger.workflowId,
1368
+ worktreePath: sessionWorktreePath,
1369
+ agent,
1370
+ stuckRepeatThreshold: STUCK_REPEAT_THRESHOLD,
1371
+ };
1372
+ }
1373
+ async function runAgentLoop(session, trigger, conversationPath) {
1374
+ const { agent, preAgentSession, sessionCtx, sessionId, handle } = session;
1375
+ const { state } = preAgentSession;
1376
+ const { emitter } = session.scope;
1377
+ const { stuckRepeatThreshold } = session;
2213
1378
  const { sessionTimeoutMs, maxTurns } = sessionCtx;
2214
1379
  const stuckConfig = {
2215
1380
  maxTurns,
2216
1381
  stuckAbortPolicy: trigger.agentConfig?.stuckAbortPolicy ?? 'abort',
2217
1382
  noProgressAbortEnabled: trigger.agentConfig?.noProgressAbortEnabled ?? false,
2218
- stuckRepeatThreshold: STUCK_REPEAT_THRESHOLD,
1383
+ stuckRepeatThreshold,
2219
1384
  };
2220
- const conversationPath = path.join(sessionsDir, `${sessionId}-conversation.jsonl`);
2221
1385
  const lastFlushedRef = { count: 0 };
2222
1386
  const unsubscribe = agent.subscribe(buildTurnEndSubscriber({
2223
1387
  agent,
@@ -2228,7 +1392,7 @@ async function runWorkflow(trigger, ctx, apiKey, daemonRegistry, emitter, steerR
2228
1392
  emitter,
2229
1393
  conversationPath,
2230
1394
  lastFlushedRef,
2231
- stuckRepeatThreshold: STUCK_REPEAT_THRESHOLD,
1395
+ stuckRepeatThreshold,
2232
1396
  }));
2233
1397
  let stopReason = 'stop';
2234
1398
  let errorMessage;
@@ -2242,7 +1406,7 @@ async function runWorkflow(trigger, ctx, apiKey, daemonRegistry, emitter, steerR
2242
1406
  reject(new Error('Workflow timed out'));
2243
1407
  }, sessionTimeoutMs);
2244
1408
  });
2245
- console.log(`[WorkflowRunner] Agent loop started: sessionId=${sessionId} workflowId=${trigger.workflowId} modelId=${modelId}`);
1409
+ console.log(`[WorkflowRunner] Agent loop started: sessionId=${sessionId} workflowId=${trigger.workflowId} modelId=${preAgentSession.modelId}`);
2246
1410
  await Promise.race([agent.prompt(buildUserMessage(sessionCtx.initialPrompt)), timeoutPromise])
2247
1411
  .catch((err) => {
2248
1412
  agent.abort();
@@ -2270,14 +1434,36 @@ async function runWorkflow(trigger, ctx, apiKey, daemonRegistry, emitter, steerR
2270
1434
  void appendConversationMessages(conversationPath, remainingMessages).catch(() => { });
2271
1435
  if (timeoutHandle !== undefined)
2272
1436
  clearTimeout(timeoutHandle);
2273
- if (state.workrailSessionId !== null) {
2274
- steerRegistry?.delete(state.workrailSessionId);
2275
- }
2276
- if (state.workrailSessionId !== null) {
2277
- abortRegistry?.delete(state.workrailSessionId);
2278
- }
1437
+ handle?.dispose();
2279
1438
  console.log(`[WorkflowRunner] Agent loop ended: sessionId=${sessionId} stopReason=${stopReason}${errorMessage ? ` error=${errorMessage.slice(0, 120)}` : ''}`);
2280
1439
  }
1440
+ if (stopReason === 'error') {
1441
+ return { kind: 'aborted', errorMessage };
1442
+ }
1443
+ return { kind: 'completed', stopReason, errorMessage };
1444
+ }
1445
+ async function runWorkflow(trigger, ctx, apiKey, daemonRegistry, emitter, activeSessionSet, _statsDir, _sessionsDir, source) {
1446
+ const statsDir = _statsDir ?? DAEMON_STATS_DIR;
1447
+ const sessionsDir = _sessionsDir ?? _shared_js_1.DAEMON_SESSIONS_DIR;
1448
+ const startMs = Date.now();
1449
+ const sessionId = (0, node_crypto_1.randomUUID)();
1450
+ console.log(`[WorkflowRunner] Session started: sessionId=${sessionId} workflowId=${trigger.workflowId}`);
1451
+ emitter?.emit({
1452
+ kind: 'session_started',
1453
+ sessionId,
1454
+ workflowId: trigger.workflowId,
1455
+ workspacePath: trigger.workspacePath,
1456
+ });
1457
+ const preResult = await buildPreAgentSession(trigger, ctx, apiKey, sessionId, startMs, statsDir, sessionsDir, emitter, daemonRegistry, activeSessionSet, source);
1458
+ if (preResult.kind === 'complete') {
1459
+ return preResult.result;
1460
+ }
1461
+ const readySession = await buildAgentReadySession(preResult.session, trigger, ctx, apiKey, sessionId, emitter, daemonRegistry, activeSessionSet);
1462
+ const conversationPath = path.join(sessionsDir, `${sessionId}-conversation.jsonl`);
1463
+ const outcome = await runAgentLoop(readySession, trigger, conversationPath);
1464
+ const stopReason = outcome.kind === 'aborted' ? 'error' : outcome.stopReason;
1465
+ const errorMessage = outcome.errorMessage;
1466
+ const { state, sessionWorktreePath } = readySession.preAgentSession;
2281
1467
  const finalizationCtx = {
2282
1468
  sessionId,
2283
1469
  workrailSessionId: state.workrailSessionId,