@exaudeus/workrail 3.31.1 → 3.33.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (82) hide show
  1. package/dist/cli/commands/index.d.ts +1 -0
  2. package/dist/cli/commands/index.js +3 -1
  3. package/dist/cli/commands/worktrain-await.js +11 -9
  4. package/dist/cli/commands/worktrain-daemon-install.d.ts +35 -0
  5. package/dist/cli/commands/worktrain-daemon-install.js +291 -0
  6. package/dist/cli/commands/worktrain-daemon.d.ts +31 -0
  7. package/dist/cli/commands/worktrain-daemon.js +272 -0
  8. package/dist/cli/commands/worktrain-spawn.js +11 -9
  9. package/dist/cli-worktrain.js +329 -0
  10. package/dist/cli.js +4 -22
  11. package/dist/console/standalone-console.d.ts +28 -0
  12. package/dist/console/standalone-console.js +142 -0
  13. package/dist/{console/assets/index-6H9DeFxj.js → console-ui/assets/index-BuJFLLfY.js} +1 -1
  14. package/dist/{console → console-ui}/index.html +1 -1
  15. package/dist/daemon/agent-loop.d.ts +26 -0
  16. package/dist/daemon/agent-loop.js +53 -2
  17. package/dist/daemon/daemon-events.d.ts +103 -0
  18. package/dist/daemon/daemon-events.js +56 -0
  19. package/dist/daemon/workflow-runner.d.ts +6 -3
  20. package/dist/daemon/workflow-runner.js +229 -33
  21. package/dist/infrastructure/session/HttpServer.js +133 -34
  22. package/dist/manifest.json +134 -70
  23. package/dist/mcp/output-schemas.d.ts +30 -30
  24. package/dist/mcp/transports/bridge-events.d.ts +4 -0
  25. package/dist/mcp/transports/fatal-exit.js +4 -0
  26. package/dist/mcp/transports/http-entry.js +2 -0
  27. package/dist/mcp/transports/stdio-entry.js +26 -6
  28. package/dist/mcp/v2/tools.d.ts +4 -4
  29. package/dist/trigger/adapters/github-poller.d.ts +44 -0
  30. package/dist/trigger/adapters/github-poller.js +190 -0
  31. package/dist/trigger/adapters/gitlab-poller.d.ts +27 -0
  32. package/dist/trigger/adapters/gitlab-poller.js +81 -0
  33. package/dist/trigger/delivery-client.d.ts +2 -1
  34. package/dist/trigger/delivery-client.js +4 -1
  35. package/dist/trigger/index.d.ts +4 -1
  36. package/dist/trigger/index.js +5 -1
  37. package/dist/trigger/polled-event-store.d.ts +22 -0
  38. package/dist/trigger/polled-event-store.js +173 -0
  39. package/dist/trigger/polling-scheduler.d.ts +20 -0
  40. package/dist/trigger/polling-scheduler.js +249 -0
  41. package/dist/trigger/trigger-listener.d.ts +5 -0
  42. package/dist/trigger/trigger-listener.js +53 -4
  43. package/dist/trigger/trigger-router.d.ts +4 -2
  44. package/dist/trigger/trigger-router.js +7 -4
  45. package/dist/trigger/trigger-store.js +114 -33
  46. package/dist/trigger/types.d.ts +17 -1
  47. package/dist/v2/durable-core/schemas/export-bundle/index.d.ts +224 -224
  48. package/dist/v2/durable-core/schemas/session/events.d.ts +42 -42
  49. package/dist/v2/durable-core/schemas/session/manifest.d.ts +6 -6
  50. package/dist/v2/durable-core/schemas/session/validation-event.d.ts +2 -2
  51. package/dist/v2/durable-core/tokens/payloads.d.ts +52 -52
  52. package/dist/v2/usecases/console-routes.js +3 -3
  53. package/dist/v2/usecases/console-service.js +133 -9
  54. package/dist/v2/usecases/console-types.d.ts +7 -0
  55. package/docs/design/daemon-conversation-logging-plan.md +98 -0
  56. package/docs/design/daemon-conversation-logging-review.md +55 -0
  57. package/docs/design/daemon-conversation-logging.md +129 -0
  58. package/docs/design/github-polling-adapter-design-candidates.md +226 -0
  59. package/docs/design/github-polling-adapter-design-review-findings.md +131 -0
  60. package/docs/design/github-polling-adapter-implementation-plan.md +284 -0
  61. package/docs/design/implementation_plan.md +192 -0
  62. package/docs/design/workflow-id-validation-at-startup.md +146 -0
  63. package/docs/design/workflow-id-validation-design-review.md +87 -0
  64. package/docs/design/workflow-id-validation-implementation-plan.md +185 -0
  65. package/docs/design/worktrain-system-prompt-report-issue-candidates.md +135 -0
  66. package/docs/design/worktrain-system-prompt-report-issue-design-review.md +73 -0
  67. package/docs/ideas/backlog.md +465 -0
  68. package/package.json +1 -1
  69. package/workflows/architecture-scalability-audit.json +1 -1
  70. package/workflows/bug-investigation.agentic.v2.json +3 -3
  71. package/workflows/coding-task-workflow-agentic.json +32 -32
  72. package/workflows/coding-task-workflow-agentic.lean.v2.json +1 -1
  73. package/workflows/coding-task-workflow-agentic.v2.json +7 -7
  74. package/workflows/mr-review-workflow.agentic.v2.json +21 -12
  75. package/workflows/personal-learning-materials-creation-branched.json +2 -2
  76. package/workflows/production-readiness-audit.json +1 -1
  77. package/workflows/relocation-workflow-us.json +2 -2
  78. package/workflows/ui-ux-design-workflow.json +14 -14
  79. package/workflows/workflow-for-workflows.json +3 -3
  80. package/workflows/workflow-for-workflows.v2.json +2 -2
  81. package/workflows/wr.discovery.json +1 -1
  82. /package/dist/{console → console-ui}/assets/index-8dh0Psu-.css +0 -0
@@ -42,6 +42,7 @@ exports.readAllDaemonSessions = readAllDaemonSessions;
42
42
  exports.runStartupRecovery = runStartupRecovery;
43
43
  exports.makeContinueWorkflowTool = makeContinueWorkflowTool;
44
44
  exports.makeBashTool = makeBashTool;
45
+ exports.makeReportIssueTool = makeReportIssueTool;
45
46
  exports.buildSessionRecap = buildSessionRecap;
46
47
  exports.buildSystemPrompt = buildSystemPrompt;
47
48
  exports.runWorkflow = runWorkflow;
@@ -338,7 +339,7 @@ function getSchemas() {
338
339
  };
339
340
  return _schemas;
340
341
  }
341
- function makeContinueWorkflowTool(sessionId, ctx, onAdvance, onComplete, schemas, _executeContinueWorkflowFn = index_js_1.executeContinueWorkflow) {
342
+ function makeContinueWorkflowTool(sessionId, ctx, onAdvance, onComplete, schemas, _executeContinueWorkflowFn = index_js_1.executeContinueWorkflow, emitter, workrailSessionId) {
342
343
  return {
343
344
  name: 'continue_workflow',
344
345
  description: 'Advance the WorkRail workflow to the next step. Call this after completing all work ' +
@@ -347,6 +348,7 @@ function makeContinueWorkflowTool(sessionId, ctx, onAdvance, onComplete, schemas
347
348
  label: 'Continue Workflow',
348
349
  execute: async (_toolCallId, params) => {
349
350
  console.log(`[WorkflowRunner] Tool: continue_workflow sessionId=${sessionId}`);
351
+ emitter?.emit({ kind: 'tool_called', sessionId, toolName: 'continue_workflow', summary: params.intent ?? 'advance', ...(workrailSessionId != null ? { workrailSessionId } : {}) });
350
352
  const result = await _executeContinueWorkflowFn({
351
353
  continueToken: params.continueToken,
352
354
  intent: (params.intent ?? 'advance'),
@@ -425,7 +427,7 @@ function makeContinueWorkflowTool(sessionId, ctx, onAdvance, onComplete, schemas
425
427
  },
426
428
  };
427
429
  }
428
- function makeBashTool(workspacePath, schemas) {
430
+ function makeBashTool(workspacePath, schemas, sessionId, emitter, workrailSessionId) {
429
431
  return {
430
432
  name: 'Bash',
431
433
  description: 'Execute a shell command. Throws on failure (non-zero exit with stderr, or exit code 2+). ' +
@@ -436,11 +438,14 @@ function makeBashTool(workspacePath, schemas) {
436
438
  label: 'Bash',
437
439
  execute: async (_toolCallId, params) => {
438
440
  console.log(`[WorkflowRunner] Tool: bash "${String(params.command).slice(0, 80)}"`);
441
+ if (sessionId)
442
+ emitter?.emit({ kind: 'tool_called', sessionId, toolName: 'Bash', summary: String(params.command).slice(0, 80), ...(workrailSessionId != null ? { workrailSessionId } : {}) });
439
443
  const cwd = params.cwd ?? workspacePath;
440
444
  try {
441
445
  const { stdout, stderr } = await execAsync(params.command, {
442
446
  cwd,
443
447
  timeout: BASH_TIMEOUT_MS,
448
+ shell: '/bin/bash',
444
449
  });
445
450
  const output = [stdout, stderr].filter(Boolean).join('\n');
446
451
  return {
@@ -470,13 +475,15 @@ function makeBashTool(workspacePath, schemas) {
470
475
  },
471
476
  };
472
477
  }
473
- function makeReadTool(schemas) {
478
+ function makeReadTool(schemas, sessionId, emitter, workrailSessionId) {
474
479
  return {
475
480
  name: 'Read',
476
481
  description: 'Read the contents of a file at the given absolute path.',
477
482
  inputSchema: schemas['ReadParams'],
478
483
  label: 'Read',
479
484
  execute: async (_toolCallId, params) => {
485
+ if (sessionId)
486
+ emitter?.emit({ kind: 'tool_called', sessionId, toolName: 'Read', summary: String(params.filePath).slice(0, 80), ...(workrailSessionId != null ? { workrailSessionId } : {}) });
480
487
  const content = await fs.readFile(params.filePath, 'utf8');
481
488
  return {
482
489
  content: [{ type: 'text', text: content }],
@@ -485,13 +492,15 @@ function makeReadTool(schemas) {
485
492
  },
486
493
  };
487
494
  }
488
- function makeWriteTool(schemas) {
495
+ function makeWriteTool(schemas, sessionId, emitter, workrailSessionId) {
489
496
  return {
490
497
  name: 'Write',
491
498
  description: 'Write content to a file at the given absolute path. Creates parent directories if needed.',
492
499
  inputSchema: schemas['WriteParams'],
493
500
  label: 'Write',
494
501
  execute: async (_toolCallId, params) => {
502
+ if (sessionId)
503
+ emitter?.emit({ kind: 'tool_called', sessionId, toolName: 'Write', summary: String(params.filePath).slice(0, 80), ...(workrailSessionId != null ? { workrailSessionId } : {}) });
495
504
  await fs.mkdir(path.dirname(params.filePath), { recursive: true });
496
505
  await fs.writeFile(params.filePath, params.content, 'utf8');
497
506
  return {
@@ -501,6 +510,140 @@ function makeWriteTool(schemas) {
501
510
  },
502
511
  };
503
512
  }
513
+ async function appendIssueAsync(issuesDir, sessionId, record) {
514
+ await fs.mkdir(issuesDir, { recursive: true });
515
+ const filePath = path.join(issuesDir, `${sessionId}.jsonl`);
516
+ const line = JSON.stringify({ ...record, ts: Date.now() }) + '\n';
517
+ await fs.appendFile(filePath, line, 'utf8');
518
+ }
519
+ function makeReportIssueTool(sessionId, emitter, issuesDirOverride) {
520
+ const issuesDir = issuesDirOverride ?? path.join(os.homedir(), '.workrail', 'issues');
521
+ return {
522
+ name: 'report_issue',
523
+ description: "Record a structured issue, error, or unexpected behavior. Call this AND continue_workflow (unless fatal). " +
524
+ "Does not stop the session -- it creates a record for the auto-fix coordinator.",
525
+ inputSchema: {
526
+ type: 'object',
527
+ properties: {
528
+ kind: {
529
+ type: 'string',
530
+ enum: ['tool_failure', 'blocked', 'unexpected_behavior', 'needs_human', 'self_correction'],
531
+ description: 'Category of issue being reported.',
532
+ },
533
+ severity: {
534
+ type: 'string',
535
+ enum: ['info', 'warn', 'error', 'fatal'],
536
+ description: 'Severity level. Fatal means the session cannot continue productively.',
537
+ },
538
+ summary: {
539
+ type: 'string',
540
+ description: 'One-line summary of the issue. Max 200 chars.',
541
+ maxLength: 200,
542
+ },
543
+ context: {
544
+ type: 'string',
545
+ description: 'What you were trying to do when this issue occurred.',
546
+ },
547
+ toolName: {
548
+ type: 'string',
549
+ description: 'Name of the tool that failed or behaved unexpectedly, if applicable.',
550
+ },
551
+ command: {
552
+ type: 'string',
553
+ description: 'The shell command or expression that caused the issue, if applicable.',
554
+ },
555
+ suggestedFix: {
556
+ type: 'string',
557
+ description: 'A suggested fix or recovery action for the auto-fix coordinator.',
558
+ },
559
+ continueToken: {
560
+ type: 'string',
561
+ description: 'The current continueToken, so the coordinator can resume this session.',
562
+ },
563
+ },
564
+ required: ['kind', 'severity', 'summary'],
565
+ additionalProperties: false,
566
+ },
567
+ label: 'report_issue',
568
+ execute: async (_toolCallId, params) => {
569
+ const record = {
570
+ sessionId,
571
+ kind: params.kind,
572
+ severity: params.severity,
573
+ summary: String(params.summary ?? '').slice(0, 200),
574
+ ...(params.context !== undefined && { context: String(params.context) }),
575
+ ...(params.toolName !== undefined && { toolName: String(params.toolName) }),
576
+ ...(params.command !== undefined && { command: String(params.command) }),
577
+ ...(params.suggestedFix !== undefined && { suggestedFix: String(params.suggestedFix) }),
578
+ ...(params.continueToken !== undefined && { continueToken: String(params.continueToken) }),
579
+ };
580
+ void appendIssueAsync(issuesDir, sessionId, record).catch(() => {
581
+ });
582
+ emitter?.emit({
583
+ kind: 'issue_reported',
584
+ sessionId,
585
+ issueKind: record.kind,
586
+ severity: record.severity,
587
+ summary: record.summary,
588
+ ...(record.continueToken !== undefined && { continueToken: record.continueToken }),
589
+ });
590
+ const isFatal = record.severity === 'fatal';
591
+ const message = isFatal
592
+ ? `FATAL issue recorded. Call continue_workflow with notes explaining the blocker, then the session will end.`
593
+ : `Issue recorded (severity=${record.severity}). Continue with your work unless this is fatal.`;
594
+ return {
595
+ content: [{ type: 'text', text: message }],
596
+ details: { sessionId, kind: record.kind, severity: record.severity },
597
+ };
598
+ },
599
+ };
600
+ }
601
+ const BASE_SYSTEM_PROMPT = `\
602
+ 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.
603
+
604
+ ## What you are
605
+ You are highly capable. You handle ambitious, multi-step tasks that require real codebase understanding. You don't hedge, ask for permission, or stop to check in. You work.
606
+
607
+ ## Your oracle (consult in this order when uncertain)
608
+ 1. The daemon soul rules (## Agent Rules and Philosophy below)
609
+ 2. AGENTS.md / CLAUDE.md in the workspace (injected below under Workspace Context)
610
+ 3. The current workflow step's prompt and guidance
611
+ 4. Local code patterns in the relevant module (grep the directory, not the whole repo)
612
+ 5. Industry best practices -- only when nothing above applies
613
+
614
+ ## Self-directed reasoning
615
+ Ask yourself questions to clarify your approach, then answer them yourself using tools before acting. Never wait for a human to answer -- you are the oracle.
616
+
617
+ Bad pattern: "I'll analyze both layers." (no justification)
618
+ Good pattern: "Question: Should I check the middleware? Answer: The workflow step says 'trace the full call chain', and the AGENTS.md says the entry point is in the middleware layer. Yes, start there."
619
+
620
+ ## Your tools
621
+ - \`continue_workflow\`: Advance to the next step. Call this after completing each step's work. Always include your notes in notesMarkdown and round-trip the continueToken exactly.
622
+ - \`Bash\`: Run shell commands. Use for building, testing, running scripts.
623
+ - \`Read\`: Read files.
624
+ - \`Write\`: Write files.
625
+ - \`report_issue\`: Record a structured issue, error, or unexpected behavior. Call this AND continue_workflow (unless fatal). Does not stop the session -- it creates a record for the auto-fix coordinator.
626
+
627
+ ## Execution contract
628
+ 1. Read the step carefully. Do ALL the work the step asks for.
629
+ 2. Call \`continue_workflow\` with your notes. Include the continueToken exactly.
630
+ 3. Repeat until the workflow reports it is complete.
631
+ 4. Do NOT skip steps. Do NOT call \`continue_workflow\` without completing the step's work.
632
+
633
+ ## The workflow is the contract
634
+ Every step must be fully completed before you call continue_workflow. The workflow step prompt is the specification of what 'done' means -- not a suggestion. Don't advance until the work is actually done.
635
+
636
+ Your cognitive mode changes per step: some steps make you a researcher, others a reviewer, others an implementer. Adopt the mode the step describes. Don't bring your own agenda.
637
+
638
+ ## Silent failure is the worst outcome
639
+ If something goes wrong: call report_issue, then continue unless severity is 'fatal'. Do NOT silently retry forever, work around failures without noting them, or pretend things worked. The issue record is how the system learns and self-heals.
640
+
641
+ ## Tools are your hands, not your voice
642
+ Don't narrate what you're about to do. Use the tool and report what you found. Token efficiency matters -- you have a wall-clock timeout.
643
+
644
+ ## You don't have a user. You have a workflow and a soul.
645
+ If you're unsure, consult the oracle above. If nothing answers the question, make a reasoned decision, call report_issue with kind='self_correction' to document it, and continue.\
646
+ `;
504
647
  function buildSessionRecap(notes) {
505
648
  if (notes.length === 0)
506
649
  return '';
@@ -511,20 +654,7 @@ function buildSessionRecap(notes) {
511
654
  }
512
655
  function buildSystemPrompt(trigger, sessionState, soulContent, workspaceContext) {
513
656
  const lines = [
514
- 'You are WorkRail Auto, an autonomous agent that executes workflows step by step.',
515
- '',
516
- '## Your tools',
517
- '- `continue_workflow`: Advance to the next step. Call this after completing each step\'s work.',
518
- ' Always include your notes in notesMarkdown and round-trip the continueToken exactly.',
519
- '- `Bash`: Run shell commands. Use for building, testing, running scripts.',
520
- '- `Read`: Read files.',
521
- '- `Write`: Write files.',
522
- '',
523
- '## Execution contract',
524
- '1. Read the step carefully. Do ALL the work the step asks for.',
525
- '2. Call `continue_workflow` with your notes. Include the continueToken exactly.',
526
- '3. Repeat until the workflow reports it is complete.',
527
- '4. Do NOT skip steps. Do NOT call `continue_workflow` without completing the step\'s work.',
657
+ BASE_SYSTEM_PROMPT,
528
658
  '',
529
659
  `<workrail_session_state>${sessionState}</workrail_session_state>`,
530
660
  '',
@@ -554,16 +684,21 @@ function buildUserMessage(text) {
554
684
  timestamp: Date.now(),
555
685
  };
556
686
  }
557
- async function runWorkflow(trigger, ctx, apiKey, daemonRegistry) {
687
+ async function runWorkflow(trigger, ctx, apiKey, daemonRegistry, emitter) {
558
688
  const sessionId = (0, node_crypto_1.randomUUID)();
559
689
  console.log(`[WorkflowRunner] Session started: sessionId=${sessionId} workflowId=${trigger.workflowId}`);
560
- daemonRegistry?.register(sessionId, trigger.workflowId);
690
+ emitter?.emit({
691
+ kind: 'session_started',
692
+ sessionId,
693
+ workflowId: trigger.workflowId,
694
+ workspacePath: trigger.workspacePath,
695
+ });
696
+ let workrailSessionId = null;
561
697
  let agentClient;
562
698
  let modelId;
563
699
  if (trigger.agentConfig?.model) {
564
700
  const slashIdx = trigger.agentConfig.model.indexOf('/');
565
701
  if (slashIdx === -1) {
566
- daemonRegistry?.unregister(sessionId, 'failed');
567
702
  return {
568
703
  _tag: 'error',
569
704
  workflowId: trigger.workflowId,
@@ -591,7 +726,9 @@ async function runWorkflow(trigger, ctx, apiKey, daemonRegistry) {
591
726
  let lastStepNotes;
592
727
  const onAdvance = (stepText, _continueToken) => {
593
728
  pendingSteerText = stepText;
594
- daemonRegistry?.heartbeat(sessionId);
729
+ if (workrailSessionId !== null)
730
+ daemonRegistry?.heartbeat(workrailSessionId);
731
+ emitter?.emit({ kind: 'step_advanced', sessionId, ...(workrailSessionId != null ? { workrailSessionId } : {}) });
595
732
  };
596
733
  const onComplete = (notes) => {
597
734
  isComplete = true;
@@ -602,9 +739,8 @@ async function runWorkflow(trigger, ctx, apiKey, daemonRegistry) {
602
739
  firstStep = trigger._preAllocatedStartResponse;
603
740
  }
604
741
  else {
605
- const startResult = await (0, start_js_1.executeStartWorkflow)({ workflowId: trigger.workflowId, workspacePath: trigger.workspacePath, goal: trigger.goal }, ctx, { is_autonomous: 'true' });
742
+ const startResult = await (0, start_js_1.executeStartWorkflow)({ workflowId: trigger.workflowId, workspacePath: trigger.workspacePath, goal: trigger.goal }, ctx, { is_autonomous: 'true', workspacePath: trigger.workspacePath });
606
743
  if (startResult.isErr()) {
607
- daemonRegistry?.unregister(sessionId, 'failed');
608
744
  return {
609
745
  _tag: 'error',
610
746
  workflowId: trigger.workflowId,
@@ -616,20 +752,35 @@ async function runWorkflow(trigger, ctx, apiKey, daemonRegistry) {
616
752
  }
617
753
  const startContinueToken = firstStep.continueToken ?? '';
618
754
  const startCheckpointToken = firstStep.checkpointToken ?? null;
755
+ if (startContinueToken) {
756
+ const decoded = await (0, v2_token_ops_js_1.parseContinueTokenOrFail)(startContinueToken, ctx.v2.tokenCodecPorts, ctx.v2.tokenAliasStore);
757
+ if (decoded.isOk()) {
758
+ workrailSessionId = decoded.value.sessionId;
759
+ }
760
+ else {
761
+ console.error(`[WorkflowRunner] Error: could not decode WorkRail session ID from continueToken -- isLive and liveActivity will not work for this session. Reason: ${decoded.error.message}`);
762
+ }
763
+ }
764
+ if (workrailSessionId !== null) {
765
+ daemonRegistry?.register(workrailSessionId, trigger.workflowId);
766
+ }
619
767
  if (startContinueToken) {
620
768
  await persistTokens(sessionId, startContinueToken, startCheckpointToken);
621
769
  }
622
770
  if (firstStep.isComplete) {
623
771
  await fs.unlink(path.join(exports.DAEMON_SESSIONS_DIR, `${sessionId}.json`)).catch(() => { });
624
- daemonRegistry?.unregister(sessionId, 'completed');
772
+ emitter?.emit({ kind: 'session_completed', sessionId, workflowId: trigger.workflowId, outcome: 'success', detail: 'stop', ...(workrailSessionId != null ? { workrailSessionId } : {}) });
773
+ if (workrailSessionId !== null)
774
+ daemonRegistry?.unregister(workrailSessionId, 'completed');
625
775
  return { _tag: 'success', workflowId: trigger.workflowId, stopReason: 'stop' };
626
776
  }
627
777
  const schemas = getSchemas();
628
778
  const tools = [
629
- makeContinueWorkflowTool(sessionId, ctx, onAdvance, onComplete, schemas),
630
- makeBashTool(trigger.workspacePath, schemas),
631
- makeReadTool(schemas),
632
- makeWriteTool(schemas),
779
+ makeContinueWorkflowTool(sessionId, ctx, onAdvance, onComplete, schemas, index_js_1.executeContinueWorkflow, emitter, workrailSessionId),
780
+ makeBashTool(trigger.workspacePath, schemas, sessionId, emitter, workrailSessionId),
781
+ makeReadTool(schemas, sessionId, emitter, workrailSessionId),
782
+ makeWriteTool(schemas, sessionId, emitter, workrailSessionId),
783
+ makeReportIssueTool(sessionId, emitter),
633
784
  ];
634
785
  const [soulContent, workspaceContext, sessionNotes] = await Promise.all([
635
786
  loadDaemonSoul(trigger.soulFile),
@@ -644,12 +795,37 @@ async function runWorkflow(trigger, ctx, apiKey, daemonRegistry) {
644
795
  `\n\ncontinueToken: ${startContinueToken}` +
645
796
  contextJson +
646
797
  '\n\nComplete all step work, then call continue_workflow with your notes to begin.';
798
+ const agentCallbacks = {
799
+ onLlmTurnStarted: ({ messageCount }) => {
800
+ emitter?.emit({ kind: 'llm_turn_started', sessionId, messageCount });
801
+ },
802
+ onLlmTurnCompleted: ({ stopReason, outputTokens, inputTokens, toolNamesRequested }) => {
803
+ emitter?.emit({
804
+ kind: 'llm_turn_completed',
805
+ sessionId,
806
+ stopReason,
807
+ outputTokens,
808
+ inputTokens,
809
+ toolNamesRequested,
810
+ });
811
+ },
812
+ onToolCallStarted: ({ toolName, argsSummary }) => {
813
+ emitter?.emit({ kind: 'tool_call_started', sessionId, toolName, argsSummary });
814
+ },
815
+ onToolCallCompleted: ({ toolName, durationMs, resultSummary }) => {
816
+ emitter?.emit({ kind: 'tool_call_completed', sessionId, toolName, durationMs, resultSummary });
817
+ },
818
+ onToolCallFailed: ({ toolName, durationMs, errorMessage }) => {
819
+ emitter?.emit({ kind: 'tool_call_failed', sessionId, toolName, durationMs, errorMessage });
820
+ },
821
+ };
647
822
  const agent = new agent_loop_js_1.AgentLoop({
648
823
  systemPrompt: buildSystemPrompt(trigger, sessionState, soulContent, workspaceContext),
649
824
  modelId,
650
825
  tools,
651
826
  client: agentClient,
652
827
  toolExecution: 'sequential',
828
+ callbacks: agentCallbacks,
653
829
  });
654
830
  const sessionTimeoutMs = (trigger.agentConfig?.maxSessionMinutes ?? DEFAULT_SESSION_TIMEOUT_MINUTES) * 60 * 1000;
655
831
  const maxTurns = trigger.agentConfig?.maxTurns ?? 0;
@@ -658,6 +834,12 @@ async function runWorkflow(trigger, ctx, apiKey, daemonRegistry) {
658
834
  const unsubscribe = agent.subscribe(async (event) => {
659
835
  if (event.type !== 'turn_end')
660
836
  return;
837
+ for (const toolResult of event.toolResults) {
838
+ if (toolResult.isError) {
839
+ const errorText = toolResult.result?.content[0]?.text ?? 'tool error';
840
+ emitter?.emit({ kind: 'tool_error', sessionId, toolName: toolResult.toolName, error: errorText.slice(0, 200), ...(workrailSessionId != null ? { workrailSessionId } : {}) });
841
+ }
842
+ }
661
843
  turnCount++;
662
844
  if (maxTurns > 0 && turnCount >= maxTurns && timeoutReason === null) {
663
845
  timeoutReason = 'max_turns';
@@ -711,7 +893,9 @@ async function runWorkflow(trigger, ctx, apiKey, daemonRegistry) {
711
893
  console.log(`[WorkflowRunner] Agent loop ended: sessionId=${sessionId} stopReason=${stopReason}${errorMessage ? ` error=${errorMessage.slice(0, 120)}` : ''}`);
712
894
  }
713
895
  if (timeoutReason !== null) {
714
- daemonRegistry?.unregister(sessionId, 'failed');
896
+ emitter?.emit({ kind: 'session_completed', sessionId, workflowId: trigger.workflowId, outcome: 'timeout', detail: timeoutReason, ...(workrailSessionId != null ? { workrailSessionId } : {}) });
897
+ if (workrailSessionId !== null)
898
+ daemonRegistry?.unregister(workrailSessionId, 'failed');
715
899
  const limitDescription = timeoutReason === 'wall_clock'
716
900
  ? `${trigger.agentConfig?.maxSessionMinutes ?? DEFAULT_SESSION_TIMEOUT_MINUTES} minutes`
717
901
  : `${trigger.agentConfig?.maxTurns} turns`;
@@ -724,17 +908,29 @@ async function runWorkflow(trigger, ctx, apiKey, daemonRegistry) {
724
908
  };
725
909
  }
726
910
  if (stopReason === 'error' || errorMessage) {
727
- daemonRegistry?.unregister(sessionId, 'failed');
911
+ const errMsg = errorMessage ?? 'Agent stopped with error reason';
912
+ emitter?.emit({ kind: 'session_completed', sessionId, workflowId: trigger.workflowId, outcome: 'error', detail: errMsg.slice(0, 200), ...(workrailSessionId != null ? { workrailSessionId } : {}) });
913
+ if (workrailSessionId !== null)
914
+ daemonRegistry?.unregister(workrailSessionId, 'failed');
915
+ const stuckMarker = `\n\nWORKTRAIN_STUCK: ${JSON.stringify({
916
+ reason: 'session_error',
917
+ error: errMsg.slice(0, 500),
918
+ workflowId: trigger.workflowId,
919
+ sessionId,
920
+ })}`;
728
921
  return {
729
922
  _tag: 'error',
730
923
  workflowId: trigger.workflowId,
731
- message: errorMessage ?? 'Agent stopped with error reason',
924
+ message: errMsg,
732
925
  stopReason,
926
+ lastStepNotes: stuckMarker,
733
927
  };
734
928
  }
735
929
  await fs.unlink(path.join(exports.DAEMON_SESSIONS_DIR, `${sessionId}.json`)).catch(() => {
736
930
  });
737
- daemonRegistry?.unregister(sessionId, 'completed');
931
+ emitter?.emit({ kind: 'session_completed', sessionId, workflowId: trigger.workflowId, outcome: 'success', detail: stopReason, ...(workrailSessionId != null ? { workrailSessionId } : {}) });
932
+ if (workrailSessionId !== null)
933
+ daemonRegistry?.unregister(workrailSessionId, 'completed');
738
934
  return {
739
935
  _tag: 'success',
740
936
  workflowId: trigger.workflowId,