@exaudeus/workrail 3.32.0 → 3.34.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 (98) 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 +488 -0
  10. package/dist/cli.js +1 -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-Cb_LO718.js → console-ui/assets/index-C1JXnwZS.js} +1 -1
  14. package/dist/{console → console-ui}/index.html +1 -1
  15. package/dist/daemon/agent-loop.d.ts +27 -0
  16. package/dist/daemon/agent-loop.js +39 -1
  17. package/dist/daemon/daemon-events.d.ts +63 -1
  18. package/dist/daemon/workflow-runner.d.ts +3 -2
  19. package/dist/daemon/workflow-runner.js +285 -46
  20. package/dist/infrastructure/session/HttpServer.js +133 -34
  21. package/dist/manifest.json +136 -104
  22. package/dist/mcp/handlers/v2-error-mapping.d.ts +3 -0
  23. package/dist/mcp/handlers/v2-error-mapping.js +2 -0
  24. package/dist/mcp/handlers/v2-execution/advance.js +25 -0
  25. package/dist/mcp/handlers/v2-execution/continue-advance.js +7 -0
  26. package/dist/mcp/output-schemas.d.ts +30 -30
  27. package/dist/mcp/transports/fatal-exit.js +4 -0
  28. package/dist/mcp/transports/http-entry.js +0 -5
  29. package/dist/mcp/transports/stdio-entry.js +24 -12
  30. package/dist/mcp/v2/tools.d.ts +4 -4
  31. package/dist/mcp-server.d.ts +0 -2
  32. package/dist/mcp-server.js +1 -42
  33. package/dist/trigger/adapters/github-poller.d.ts +44 -0
  34. package/dist/trigger/adapters/github-poller.js +190 -0
  35. package/dist/trigger/adapters/gitlab-poller.d.ts +27 -0
  36. package/dist/trigger/adapters/gitlab-poller.js +81 -0
  37. package/dist/trigger/index.d.ts +4 -1
  38. package/dist/trigger/index.js +5 -1
  39. package/dist/trigger/polled-event-store.d.ts +22 -0
  40. package/dist/trigger/polled-event-store.js +173 -0
  41. package/dist/trigger/polling-scheduler.d.ts +20 -0
  42. package/dist/trigger/polling-scheduler.js +249 -0
  43. package/dist/trigger/trigger-listener.d.ts +3 -0
  44. package/dist/trigger/trigger-listener.js +47 -3
  45. package/dist/trigger/trigger-store.js +114 -33
  46. package/dist/trigger/types.d.ts +17 -1
  47. package/dist/v2/durable-core/domain/observation-builder.d.ts +3 -0
  48. package/dist/v2/durable-core/domain/observation-builder.js +2 -2
  49. package/dist/v2/durable-core/domain/prompt-renderer.d.ts +2 -1
  50. package/dist/v2/durable-core/domain/prompt-renderer.js +10 -0
  51. package/dist/v2/durable-core/schemas/export-bundle/index.d.ts +224 -224
  52. package/dist/v2/durable-core/schemas/session/events.d.ts +42 -42
  53. package/dist/v2/durable-core/schemas/session/manifest.d.ts +6 -6
  54. package/dist/v2/durable-core/schemas/session/validation-event.d.ts +2 -2
  55. package/dist/v2/durable-core/tokens/payloads.d.ts +52 -52
  56. package/dist/v2/usecases/console-routes.js +3 -3
  57. package/dist/v2/usecases/console-service.js +185 -10
  58. package/dist/v2/usecases/console-types.d.ts +8 -0
  59. package/docs/design/bridge-removal-pr-a-candidates.md +115 -0
  60. package/docs/design/bridge-removal-pr-a-design-review.md +79 -0
  61. package/docs/design/bridge-removal-pr-a-implementation-plan.md +203 -0
  62. package/docs/design/daemon-conversation-logging-plan.md +98 -0
  63. package/docs/design/daemon-conversation-logging-review.md +55 -0
  64. package/docs/design/daemon-conversation-logging.md +129 -0
  65. package/docs/design/github-polling-adapter-design-candidates.md +226 -0
  66. package/docs/design/github-polling-adapter-design-review-findings.md +131 -0
  67. package/docs/design/github-polling-adapter-implementation-plan.md +284 -0
  68. package/docs/design/implementation_plan.md +192 -0
  69. package/docs/design/workflow-id-validation-at-startup.md +146 -0
  70. package/docs/design/workflow-id-validation-design-review.md +87 -0
  71. package/docs/design/workflow-id-validation-implementation-plan.md +185 -0
  72. package/docs/design/worktrain-system-prompt-report-issue-candidates.md +135 -0
  73. package/docs/design/worktrain-system-prompt-report-issue-design-review.md +73 -0
  74. package/docs/discovery/design-candidates.md +180 -0
  75. package/docs/discovery/design-review-findings.md +110 -0
  76. package/docs/discovery/wr-discovery-goal-reframing.md +303 -0
  77. package/docs/ideas/backlog.md +627 -0
  78. package/package.json +1 -1
  79. package/workflows/architecture-scalability-audit.json +1 -1
  80. package/workflows/bug-investigation.agentic.v2.json +3 -3
  81. package/workflows/coding-task-workflow-agentic.json +32 -32
  82. package/workflows/coding-task-workflow-agentic.lean.v2.json +1 -1
  83. package/workflows/coding-task-workflow-agentic.v2.json +7 -7
  84. package/workflows/mr-review-workflow.agentic.v2.json +21 -12
  85. package/workflows/personal-learning-materials-creation-branched.json +2 -2
  86. package/workflows/production-readiness-audit.json +1 -1
  87. package/workflows/relocation-workflow-us.json +2 -2
  88. package/workflows/ui-ux-design-workflow.json +14 -14
  89. package/workflows/workflow-for-workflows.json +3 -3
  90. package/workflows/workflow-for-workflows.v2.json +2 -2
  91. package/workflows/wr.discovery.json +59 -8
  92. package/dist/mcp/transports/bridge-entry.d.ts +0 -102
  93. package/dist/mcp/transports/bridge-entry.js +0 -454
  94. package/dist/mcp/transports/bridge-events.d.ts +0 -51
  95. package/dist/mcp/transports/bridge-events.js +0 -24
  96. package/dist/mcp/transports/primary-tombstone.d.ts +0 -21
  97. package/dist/mcp/transports/primary-tombstone.js +0 -51
  98. /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;
@@ -65,6 +66,10 @@ const BASH_TIMEOUT_MS = 5 * 60 * 1000;
65
66
  const MAX_SESSION_RECAP_NOTES = 3;
66
67
  const MAX_SESSION_NOTE_CHARS = 800;
67
68
  const DEFAULT_SESSION_TIMEOUT_MINUTES = 30;
69
+ const DEFAULT_MAX_TURNS = 50;
70
+ function withWorkrailSession(sid) {
71
+ return sid != null ? { workrailSessionId: sid } : {};
72
+ }
68
73
  exports.DAEMON_SESSIONS_DIR = path.join(os.homedir(), '.workrail', 'daemon-sessions');
69
74
  const MAX_ORPHAN_AGE_MS = 2 * 60 * 60 * 1000;
70
75
  const WORKRAIL_DIR = path.join(os.homedir(), '.workrail');
@@ -304,6 +309,13 @@ function getSchemas() {
304
309
  type: 'string',
305
310
  description: 'Notes on what you did in this step (10-30 lines, markdown).',
306
311
  },
312
+ artifacts: {
313
+ type: 'array',
314
+ items: {},
315
+ description: 'Optional structured artifacts to attach to this step. ' +
316
+ 'Include wr.assessment objects here when the step requires an assessment gate. ' +
317
+ 'Example: [{ "kind": "wr.assessment", "assessmentId": "<id>", "dimensions": { "<dimensionId>": "high" } }]',
318
+ },
307
319
  context: {
308
320
  type: 'object',
309
321
  additionalProperties: true,
@@ -338,21 +350,25 @@ function getSchemas() {
338
350
  };
339
351
  return _schemas;
340
352
  }
341
- function makeContinueWorkflowTool(sessionId, ctx, onAdvance, onComplete, schemas, _executeContinueWorkflowFn = index_js_1.executeContinueWorkflow, emitter) {
353
+ function makeContinueWorkflowTool(sessionId, ctx, onAdvance, onComplete, schemas, _executeContinueWorkflowFn = index_js_1.executeContinueWorkflow, emitter, workrailSessionId) {
342
354
  return {
343
355
  name: 'continue_workflow',
344
356
  description: 'Advance the WorkRail workflow to the next step. Call this after completing all work ' +
345
- 'required by the current step. Include your notes in notesMarkdown.',
357
+ 'required by the current step. Include your notes in notesMarkdown. ' +
358
+ 'When the step requires an assessment gate, include wr.assessment objects in artifacts.',
346
359
  inputSchema: schemas['ContinueWorkflowParams'],
347
360
  label: 'Continue Workflow',
348
361
  execute: async (_toolCallId, params) => {
349
362
  console.log(`[WorkflowRunner] Tool: continue_workflow sessionId=${sessionId}`);
350
- emitter?.emit({ kind: 'tool_called', sessionId, toolName: 'continue_workflow', summary: params.intent ?? 'advance' });
363
+ emitter?.emit({ kind: 'tool_called', sessionId, toolName: 'continue_workflow', summary: params.intent ?? 'advance', ...withWorkrailSession(workrailSessionId) });
351
364
  const result = await _executeContinueWorkflowFn({
352
365
  continueToken: params.continueToken,
353
366
  intent: (params.intent ?? 'advance'),
354
- output: params.notesMarkdown
355
- ? { notesMarkdown: params.notesMarkdown }
367
+ output: (params.notesMarkdown || params.artifacts?.length)
368
+ ? {
369
+ ...(params.notesMarkdown ? { notesMarkdown: params.notesMarkdown } : {}),
370
+ ...(params.artifacts ? { artifacts: params.artifacts } : {}),
371
+ }
356
372
  : undefined,
357
373
  context: params.context,
358
374
  }, ctx);
@@ -426,7 +442,7 @@ function makeContinueWorkflowTool(sessionId, ctx, onAdvance, onComplete, schemas
426
442
  },
427
443
  };
428
444
  }
429
- function makeBashTool(workspacePath, schemas, sessionId, emitter) {
445
+ function makeBashTool(workspacePath, schemas, sessionId, emitter, workrailSessionId) {
430
446
  return {
431
447
  name: 'Bash',
432
448
  description: 'Execute a shell command. Throws on failure (non-zero exit with stderr, or exit code 2+). ' +
@@ -438,12 +454,13 @@ function makeBashTool(workspacePath, schemas, sessionId, emitter) {
438
454
  execute: async (_toolCallId, params) => {
439
455
  console.log(`[WorkflowRunner] Tool: bash "${String(params.command).slice(0, 80)}"`);
440
456
  if (sessionId)
441
- emitter?.emit({ kind: 'tool_called', sessionId, toolName: 'Bash', summary: String(params.command).slice(0, 80) });
457
+ emitter?.emit({ kind: 'tool_called', sessionId, toolName: 'Bash', summary: String(params.command).slice(0, 80), ...withWorkrailSession(workrailSessionId) });
442
458
  const cwd = params.cwd ?? workspacePath;
443
459
  try {
444
460
  const { stdout, stderr } = await execAsync(params.command, {
445
461
  cwd,
446
462
  timeout: BASH_TIMEOUT_MS,
463
+ shell: '/bin/bash',
447
464
  });
448
465
  const output = [stdout, stderr].filter(Boolean).join('\n');
449
466
  return {
@@ -473,7 +490,7 @@ function makeBashTool(workspacePath, schemas, sessionId, emitter) {
473
490
  },
474
491
  };
475
492
  }
476
- function makeReadTool(schemas, sessionId, emitter) {
493
+ function makeReadTool(schemas, sessionId, emitter, workrailSessionId) {
477
494
  return {
478
495
  name: 'Read',
479
496
  description: 'Read the contents of a file at the given absolute path.',
@@ -481,7 +498,7 @@ function makeReadTool(schemas, sessionId, emitter) {
481
498
  label: 'Read',
482
499
  execute: async (_toolCallId, params) => {
483
500
  if (sessionId)
484
- emitter?.emit({ kind: 'tool_called', sessionId, toolName: 'Read', summary: String(params.filePath).slice(0, 80) });
501
+ emitter?.emit({ kind: 'tool_called', sessionId, toolName: 'Read', summary: String(params.filePath).slice(0, 80), ...withWorkrailSession(workrailSessionId) });
485
502
  const content = await fs.readFile(params.filePath, 'utf8');
486
503
  return {
487
504
  content: [{ type: 'text', text: content }],
@@ -490,7 +507,7 @@ function makeReadTool(schemas, sessionId, emitter) {
490
507
  },
491
508
  };
492
509
  }
493
- function makeWriteTool(schemas, sessionId, emitter) {
510
+ function makeWriteTool(schemas, sessionId, emitter, workrailSessionId) {
494
511
  return {
495
512
  name: 'Write',
496
513
  description: 'Write content to a file at the given absolute path. Creates parent directories if needed.',
@@ -498,7 +515,7 @@ function makeWriteTool(schemas, sessionId, emitter) {
498
515
  label: 'Write',
499
516
  execute: async (_toolCallId, params) => {
500
517
  if (sessionId)
501
- emitter?.emit({ kind: 'tool_called', sessionId, toolName: 'Write', summary: String(params.filePath).slice(0, 80) });
518
+ emitter?.emit({ kind: 'tool_called', sessionId, toolName: 'Write', summary: String(params.filePath).slice(0, 80), ...withWorkrailSession(workrailSessionId) });
502
519
  await fs.mkdir(path.dirname(params.filePath), { recursive: true });
503
520
  await fs.writeFile(params.filePath, params.content, 'utf8');
504
521
  return {
@@ -508,6 +525,142 @@ function makeWriteTool(schemas, sessionId, emitter) {
508
525
  },
509
526
  };
510
527
  }
528
+ async function appendIssueAsync(issuesDir, sessionId, record) {
529
+ await fs.mkdir(issuesDir, { recursive: true });
530
+ const filePath = path.join(issuesDir, `${sessionId}.jsonl`);
531
+ const line = JSON.stringify({ ...record, ts: Date.now() }) + '\n';
532
+ await fs.appendFile(filePath, line, 'utf8');
533
+ }
534
+ function makeReportIssueTool(sessionId, emitter, workrailSessionId, issuesDirOverride, onIssueSummary) {
535
+ const issuesDir = issuesDirOverride ?? path.join(os.homedir(), '.workrail', 'issues');
536
+ return {
537
+ name: 'report_issue',
538
+ description: "Record a structured issue, error, or unexpected behavior. Call this AND continue_workflow (unless fatal). " +
539
+ "Does not stop the session -- it creates a record for the auto-fix coordinator.",
540
+ inputSchema: {
541
+ type: 'object',
542
+ properties: {
543
+ kind: {
544
+ type: 'string',
545
+ enum: ['tool_failure', 'blocked', 'unexpected_behavior', 'needs_human', 'self_correction'],
546
+ description: 'Category of issue being reported.',
547
+ },
548
+ severity: {
549
+ type: 'string',
550
+ enum: ['info', 'warn', 'error', 'fatal'],
551
+ description: 'Severity level. Fatal means the session cannot continue productively.',
552
+ },
553
+ summary: {
554
+ type: 'string',
555
+ description: 'One-line summary of the issue. Max 200 chars.',
556
+ maxLength: 200,
557
+ },
558
+ context: {
559
+ type: 'string',
560
+ description: 'What you were trying to do when this issue occurred.',
561
+ },
562
+ toolName: {
563
+ type: 'string',
564
+ description: 'Name of the tool that failed or behaved unexpectedly, if applicable.',
565
+ },
566
+ command: {
567
+ type: 'string',
568
+ description: 'The shell command or expression that caused the issue, if applicable.',
569
+ },
570
+ suggestedFix: {
571
+ type: 'string',
572
+ description: 'A suggested fix or recovery action for the auto-fix coordinator.',
573
+ },
574
+ continueToken: {
575
+ type: 'string',
576
+ description: 'The current continueToken, so the coordinator can resume this session.',
577
+ },
578
+ },
579
+ required: ['kind', 'severity', 'summary'],
580
+ additionalProperties: false,
581
+ },
582
+ label: 'report_issue',
583
+ execute: async (_toolCallId, params) => {
584
+ const record = {
585
+ sessionId,
586
+ kind: params.kind,
587
+ severity: params.severity,
588
+ summary: String(params.summary ?? '').slice(0, 200),
589
+ ...(params.context !== undefined && { context: String(params.context) }),
590
+ ...(params.toolName !== undefined && { toolName: String(params.toolName) }),
591
+ ...(params.command !== undefined && { command: String(params.command) }),
592
+ ...(params.suggestedFix !== undefined && { suggestedFix: String(params.suggestedFix) }),
593
+ ...(params.continueToken !== undefined && { continueToken: String(params.continueToken) }),
594
+ };
595
+ void appendIssueAsync(issuesDir, sessionId, record).catch(() => {
596
+ });
597
+ emitter?.emit({
598
+ kind: 'issue_reported',
599
+ sessionId,
600
+ issueKind: record.kind,
601
+ severity: record.severity,
602
+ summary: record.summary,
603
+ ...(record.continueToken !== undefined && { continueToken: record.continueToken }),
604
+ ...(workrailSessionId != null ? { workrailSessionId } : {}),
605
+ });
606
+ onIssueSummary?.(record.summary);
607
+ const isFatal = record.severity === 'fatal';
608
+ const message = isFatal
609
+ ? `FATAL issue recorded. Call continue_workflow with notes explaining the blocker, then the session will end.`
610
+ : `Issue recorded (severity=${record.severity}). Continue with your work unless this is fatal.`;
611
+ return {
612
+ content: [{ type: 'text', text: message }],
613
+ details: { sessionId, kind: record.kind, severity: record.severity },
614
+ };
615
+ },
616
+ };
617
+ }
618
+ const BASE_SYSTEM_PROMPT = `\
619
+ 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.
620
+
621
+ ## What you are
622
+ 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.
623
+
624
+ ## Your oracle (consult in this order when uncertain)
625
+ 1. The daemon soul rules (## Agent Rules and Philosophy below)
626
+ 2. AGENTS.md / CLAUDE.md in the workspace (injected below under Workspace Context)
627
+ 3. The current workflow step's prompt and guidance
628
+ 4. Local code patterns in the relevant module (grep the directory, not the whole repo)
629
+ 5. Industry best practices -- only when nothing above applies
630
+
631
+ ## Self-directed reasoning
632
+ 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.
633
+
634
+ Bad pattern: "I'll analyze both layers." (no justification)
635
+ 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."
636
+
637
+ ## Your tools
638
+ - \`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.
639
+ - \`Bash\`: Run shell commands. Use for building, testing, running scripts.
640
+ - \`Read\`: Read files.
641
+ - \`Write\`: Write files.
642
+ - \`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.
643
+
644
+ ## Execution contract
645
+ 1. Read the step carefully. Do ALL the work the step asks for.
646
+ 2. Call \`continue_workflow\` with your notes. Include the continueToken exactly.
647
+ 3. Repeat until the workflow reports it is complete.
648
+ 4. Do NOT skip steps. Do NOT call \`continue_workflow\` without completing the step's work.
649
+
650
+ ## The workflow is the contract
651
+ 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.
652
+
653
+ 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.
654
+
655
+ ## Silent failure is the worst outcome
656
+ 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.
657
+
658
+ ## Tools are your hands, not your voice
659
+ 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.
660
+
661
+ ## You don't have a user. You have a workflow and a soul.
662
+ 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.\
663
+ `;
511
664
  function buildSessionRecap(notes) {
512
665
  if (notes.length === 0)
513
666
  return '';
@@ -518,20 +671,7 @@ function buildSessionRecap(notes) {
518
671
  }
519
672
  function buildSystemPrompt(trigger, sessionState, soulContent, workspaceContext) {
520
673
  const lines = [
521
- 'You are WorkRail Auto, an autonomous agent that executes workflows step by step.',
522
- '',
523
- '## Your tools',
524
- '- `continue_workflow`: Advance to the next step. Call this after completing each step\'s work.',
525
- ' Always include your notes in notesMarkdown and round-trip the continueToken exactly.',
526
- '- `Bash`: Run shell commands. Use for building, testing, running scripts.',
527
- '- `Read`: Read files.',
528
- '- `Write`: Write files.',
529
- '',
530
- '## Execution contract',
531
- '1. Read the step carefully. Do ALL the work the step asks for.',
532
- '2. Call `continue_workflow` with your notes. Include the continueToken exactly.',
533
- '3. Repeat until the workflow reports it is complete.',
534
- '4. Do NOT skip steps. Do NOT call `continue_workflow` without completing the step\'s work.',
674
+ BASE_SYSTEM_PROMPT,
535
675
  '',
536
676
  `<workrail_session_state>${sessionState}</workrail_session_state>`,
537
677
  '',
@@ -570,13 +710,12 @@ async function runWorkflow(trigger, ctx, apiKey, daemonRegistry, emitter) {
570
710
  workflowId: trigger.workflowId,
571
711
  workspacePath: trigger.workspacePath,
572
712
  });
573
- daemonRegistry?.register(sessionId, trigger.workflowId);
713
+ let workrailSessionId = null;
574
714
  let agentClient;
575
715
  let modelId;
576
716
  if (trigger.agentConfig?.model) {
577
717
  const slashIdx = trigger.agentConfig.model.indexOf('/');
578
718
  if (slashIdx === -1) {
579
- daemonRegistry?.unregister(sessionId, 'failed');
580
719
  return {
581
720
  _tag: 'error',
582
721
  workflowId: trigger.workflowId,
@@ -602,10 +741,17 @@ async function runWorkflow(trigger, ctx, apiKey, daemonRegistry, emitter) {
602
741
  let isComplete = false;
603
742
  let pendingSteerText = null;
604
743
  let lastStepNotes;
744
+ let stepAdvanceCount = 0;
745
+ const lastNToolCalls = [];
746
+ const STUCK_REPEAT_THRESHOLD = 3;
747
+ const issueSummaries = [];
748
+ const MAX_ISSUE_SUMMARIES = 10;
605
749
  const onAdvance = (stepText, _continueToken) => {
606
750
  pendingSteerText = stepText;
607
- daemonRegistry?.heartbeat(sessionId);
608
- emitter?.emit({ kind: 'step_advanced', sessionId });
751
+ stepAdvanceCount++;
752
+ if (workrailSessionId !== null)
753
+ daemonRegistry?.heartbeat(workrailSessionId);
754
+ emitter?.emit({ kind: 'step_advanced', sessionId, ...withWorkrailSession(workrailSessionId) });
609
755
  };
610
756
  const onComplete = (notes) => {
611
757
  isComplete = true;
@@ -616,9 +762,8 @@ async function runWorkflow(trigger, ctx, apiKey, daemonRegistry, emitter) {
616
762
  firstStep = trigger._preAllocatedStartResponse;
617
763
  }
618
764
  else {
619
- const startResult = await (0, start_js_1.executeStartWorkflow)({ workflowId: trigger.workflowId, workspacePath: trigger.workspacePath, goal: trigger.goal }, ctx, { is_autonomous: 'true' });
765
+ const startResult = await (0, start_js_1.executeStartWorkflow)({ workflowId: trigger.workflowId, workspacePath: trigger.workspacePath, goal: trigger.goal }, ctx, { is_autonomous: 'true', workspacePath: trigger.workspacePath });
620
766
  if (startResult.isErr()) {
621
- daemonRegistry?.unregister(sessionId, 'failed');
622
767
  return {
623
768
  _tag: 'error',
624
769
  workflowId: trigger.workflowId,
@@ -630,21 +775,39 @@ async function runWorkflow(trigger, ctx, apiKey, daemonRegistry, emitter) {
630
775
  }
631
776
  const startContinueToken = firstStep.continueToken ?? '';
632
777
  const startCheckpointToken = firstStep.checkpointToken ?? null;
778
+ if (startContinueToken) {
779
+ const decoded = await (0, v2_token_ops_js_1.parseContinueTokenOrFail)(startContinueToken, ctx.v2.tokenCodecPorts, ctx.v2.tokenAliasStore);
780
+ if (decoded.isOk()) {
781
+ workrailSessionId = decoded.value.sessionId;
782
+ }
783
+ else {
784
+ 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}`);
785
+ }
786
+ }
787
+ if (workrailSessionId !== null) {
788
+ daemonRegistry?.register(workrailSessionId, trigger.workflowId);
789
+ }
633
790
  if (startContinueToken) {
634
791
  await persistTokens(sessionId, startContinueToken, startCheckpointToken);
635
792
  }
636
793
  if (firstStep.isComplete) {
637
794
  await fs.unlink(path.join(exports.DAEMON_SESSIONS_DIR, `${sessionId}.json`)).catch(() => { });
638
- emitter?.emit({ kind: 'session_completed', sessionId, workflowId: trigger.workflowId, outcome: 'success', detail: 'stop' });
639
- daemonRegistry?.unregister(sessionId, 'completed');
795
+ emitter?.emit({ kind: 'session_completed', sessionId, workflowId: trigger.workflowId, outcome: 'success', detail: 'stop', ...withWorkrailSession(workrailSessionId) });
796
+ if (workrailSessionId !== null)
797
+ daemonRegistry?.unregister(workrailSessionId, 'completed');
640
798
  return { _tag: 'success', workflowId: trigger.workflowId, stopReason: 'stop' };
641
799
  }
642
800
  const schemas = getSchemas();
643
801
  const tools = [
644
- makeContinueWorkflowTool(sessionId, ctx, onAdvance, onComplete, schemas, index_js_1.executeContinueWorkflow, emitter),
645
- makeBashTool(trigger.workspacePath, schemas, sessionId, emitter),
646
- makeReadTool(schemas, sessionId, emitter),
647
- makeWriteTool(schemas, sessionId, emitter),
802
+ makeContinueWorkflowTool(sessionId, ctx, onAdvance, onComplete, schemas, index_js_1.executeContinueWorkflow, emitter, workrailSessionId),
803
+ makeBashTool(trigger.workspacePath, schemas, sessionId, emitter, workrailSessionId),
804
+ makeReadTool(schemas, sessionId, emitter, workrailSessionId),
805
+ makeWriteTool(schemas, sessionId, emitter, workrailSessionId),
806
+ makeReportIssueTool(sessionId, emitter, workrailSessionId, undefined, (summary) => {
807
+ if (issueSummaries.length < MAX_ISSUE_SUMMARIES) {
808
+ issueSummaries.push(summary);
809
+ }
810
+ }),
648
811
  ];
649
812
  const [soulContent, workspaceContext, sessionNotes] = await Promise.all([
650
813
  loadDaemonSoul(trigger.soulFile),
@@ -659,15 +822,51 @@ async function runWorkflow(trigger, ctx, apiKey, daemonRegistry, emitter) {
659
822
  `\n\ncontinueToken: ${startContinueToken}` +
660
823
  contextJson +
661
824
  '\n\nComplete all step work, then call continue_workflow with your notes to begin.';
825
+ const agentCallbacks = {
826
+ onLlmTurnStarted: ({ messageCount }) => {
827
+ emitter?.emit({
828
+ kind: 'llm_turn_started',
829
+ sessionId,
830
+ messageCount,
831
+ modelId,
832
+ ...withWorkrailSession(workrailSessionId),
833
+ });
834
+ },
835
+ onLlmTurnCompleted: ({ stopReason, outputTokens, inputTokens, toolNamesRequested }) => {
836
+ emitter?.emit({
837
+ kind: 'llm_turn_completed',
838
+ sessionId,
839
+ stopReason,
840
+ outputTokens,
841
+ inputTokens,
842
+ toolNamesRequested,
843
+ ...withWorkrailSession(workrailSessionId),
844
+ });
845
+ },
846
+ onToolCallStarted: ({ toolName, argsSummary }) => {
847
+ emitter?.emit({ kind: 'tool_call_started', sessionId, toolName, argsSummary, ...withWorkrailSession(workrailSessionId) });
848
+ lastNToolCalls.push({ toolName, argsSummary });
849
+ if (lastNToolCalls.length > STUCK_REPEAT_THRESHOLD) {
850
+ lastNToolCalls.shift();
851
+ }
852
+ },
853
+ onToolCallCompleted: ({ toolName, durationMs, resultSummary }) => {
854
+ emitter?.emit({ kind: 'tool_call_completed', sessionId, toolName, durationMs, resultSummary, ...withWorkrailSession(workrailSessionId) });
855
+ },
856
+ onToolCallFailed: ({ toolName, durationMs, errorMessage }) => {
857
+ emitter?.emit({ kind: 'tool_call_failed', sessionId, toolName, durationMs, errorMessage, ...withWorkrailSession(workrailSessionId) });
858
+ },
859
+ };
662
860
  const agent = new agent_loop_js_1.AgentLoop({
663
861
  systemPrompt: buildSystemPrompt(trigger, sessionState, soulContent, workspaceContext),
664
862
  modelId,
665
863
  tools,
666
864
  client: agentClient,
667
865
  toolExecution: 'sequential',
866
+ callbacks: agentCallbacks,
668
867
  });
669
868
  const sessionTimeoutMs = (trigger.agentConfig?.maxSessionMinutes ?? DEFAULT_SESSION_TIMEOUT_MINUTES) * 60 * 1000;
670
- const maxTurns = trigger.agentConfig?.maxTurns ?? 0;
869
+ const maxTurns = trigger.agentConfig?.maxTurns ?? DEFAULT_MAX_TURNS;
671
870
  let timeoutReason = null;
672
871
  let turnCount = 0;
673
872
  const unsubscribe = agent.subscribe(async (event) => {
@@ -676,7 +875,7 @@ async function runWorkflow(trigger, ctx, apiKey, daemonRegistry, emitter) {
676
875
  for (const toolResult of event.toolResults) {
677
876
  if (toolResult.isError) {
678
877
  const errorText = toolResult.result?.content[0]?.text ?? 'tool error';
679
- emitter?.emit({ kind: 'tool_error', sessionId, toolName: toolResult.toolName, error: errorText.slice(0, 200) });
878
+ emitter?.emit({ kind: 'tool_error', sessionId, toolName: toolResult.toolName, error: errorText.slice(0, 200), ...withWorkrailSession(workrailSessionId) });
680
879
  }
681
880
  }
682
881
  turnCount++;
@@ -685,6 +884,38 @@ async function runWorkflow(trigger, ctx, apiKey, daemonRegistry, emitter) {
685
884
  agent.abort();
686
885
  return;
687
886
  }
887
+ if (lastNToolCalls.length === STUCK_REPEAT_THRESHOLD &&
888
+ lastNToolCalls.every((c) => c.toolName === lastNToolCalls[0]?.toolName && c.argsSummary === lastNToolCalls[0]?.argsSummary)) {
889
+ emitter?.emit({
890
+ kind: 'agent_stuck',
891
+ sessionId,
892
+ reason: 'repeated_tool_call',
893
+ detail: `Same tool+args called ${STUCK_REPEAT_THRESHOLD} times: ${lastNToolCalls[0]?.toolName ?? 'unknown'}`,
894
+ toolName: lastNToolCalls[0]?.toolName,
895
+ argsSummary: lastNToolCalls[0]?.argsSummary,
896
+ ...withWorkrailSession(workrailSessionId),
897
+ });
898
+ }
899
+ if (maxTurns > 0 &&
900
+ turnCount >= Math.floor(maxTurns * 0.8) &&
901
+ stepAdvanceCount === 0) {
902
+ emitter?.emit({
903
+ kind: 'agent_stuck',
904
+ sessionId,
905
+ reason: 'no_progress',
906
+ detail: `${turnCount} turns used, 0 step advances (${maxTurns} turn limit)`,
907
+ ...withWorkrailSession(workrailSessionId),
908
+ });
909
+ }
910
+ if (timeoutReason !== null) {
911
+ emitter?.emit({
912
+ kind: 'agent_stuck',
913
+ sessionId,
914
+ reason: 'timeout_imminent',
915
+ detail: `${timeoutReason === 'wall_clock' ? 'Wall-clock timeout' : 'Max-turn limit'} reached`,
916
+ ...withWorkrailSession(workrailSessionId),
917
+ });
918
+ }
688
919
  if (pendingSteerText !== null && !isComplete) {
689
920
  const text = pendingSteerText;
690
921
  pendingSteerText = null;
@@ -732,11 +963,12 @@ async function runWorkflow(trigger, ctx, apiKey, daemonRegistry, emitter) {
732
963
  console.log(`[WorkflowRunner] Agent loop ended: sessionId=${sessionId} stopReason=${stopReason}${errorMessage ? ` error=${errorMessage.slice(0, 120)}` : ''}`);
733
964
  }
734
965
  if (timeoutReason !== null) {
735
- emitter?.emit({ kind: 'session_completed', sessionId, workflowId: trigger.workflowId, outcome: 'timeout', detail: timeoutReason });
736
- daemonRegistry?.unregister(sessionId, 'failed');
966
+ emitter?.emit({ kind: 'session_completed', sessionId, workflowId: trigger.workflowId, outcome: 'timeout', detail: timeoutReason, ...withWorkrailSession(workrailSessionId) });
967
+ if (workrailSessionId !== null)
968
+ daemonRegistry?.unregister(workrailSessionId, 'failed');
737
969
  const limitDescription = timeoutReason === 'wall_clock'
738
970
  ? `${trigger.agentConfig?.maxSessionMinutes ?? DEFAULT_SESSION_TIMEOUT_MINUTES} minutes`
739
- : `${trigger.agentConfig?.maxTurns} turns`;
971
+ : `${trigger.agentConfig?.maxTurns ?? DEFAULT_MAX_TURNS} turns`;
740
972
  return {
741
973
  _tag: 'timeout',
742
974
  workflowId: trigger.workflowId,
@@ -747,13 +979,19 @@ async function runWorkflow(trigger, ctx, apiKey, daemonRegistry, emitter) {
747
979
  }
748
980
  if (stopReason === 'error' || errorMessage) {
749
981
  const errMsg = errorMessage ?? 'Agent stopped with error reason';
750
- emitter?.emit({ kind: 'session_completed', sessionId, workflowId: trigger.workflowId, outcome: 'error', detail: errMsg.slice(0, 200) });
751
- daemonRegistry?.unregister(sessionId, 'failed');
982
+ emitter?.emit({ kind: 'session_completed', sessionId, workflowId: trigger.workflowId, outcome: 'error', detail: errMsg.slice(0, 200), ...withWorkrailSession(workrailSessionId) });
983
+ if (workrailSessionId !== null)
984
+ daemonRegistry?.unregister(workrailSessionId, 'failed');
985
+ const lastToolCalled = lastNToolCalls.length > 0 ? lastNToolCalls[lastNToolCalls.length - 1] : null;
752
986
  const stuckMarker = `\n\nWORKTRAIN_STUCK: ${JSON.stringify({
753
987
  reason: 'session_error',
754
988
  error: errMsg.slice(0, 500),
755
989
  workflowId: trigger.workflowId,
756
990
  sessionId,
991
+ turnCount,
992
+ stepAdvanceCount,
993
+ ...(lastToolCalled !== null && { lastToolCalled }),
994
+ ...(issueSummaries.length > 0 && { issueSummaries }),
757
995
  })}`;
758
996
  return {
759
997
  _tag: 'error',
@@ -765,8 +1003,9 @@ async function runWorkflow(trigger, ctx, apiKey, daemonRegistry, emitter) {
765
1003
  }
766
1004
  await fs.unlink(path.join(exports.DAEMON_SESSIONS_DIR, `${sessionId}.json`)).catch(() => {
767
1005
  });
768
- emitter?.emit({ kind: 'session_completed', sessionId, workflowId: trigger.workflowId, outcome: 'success', detail: stopReason });
769
- daemonRegistry?.unregister(sessionId, 'completed');
1006
+ emitter?.emit({ kind: 'session_completed', sessionId, workflowId: trigger.workflowId, outcome: 'success', detail: stopReason, ...withWorkrailSession(workrailSessionId) });
1007
+ if (workrailSessionId !== null)
1008
+ daemonRegistry?.unregister(workrailSessionId, 'completed');
770
1009
  return {
771
1010
  _tag: 'success',
772
1011
  workflowId: trigger.workflowId,