@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.
- package/dist/cli/commands/index.d.ts +1 -0
- package/dist/cli/commands/index.js +3 -1
- package/dist/cli/commands/worktrain-await.js +11 -9
- package/dist/cli/commands/worktrain-daemon-install.d.ts +35 -0
- package/dist/cli/commands/worktrain-daemon-install.js +291 -0
- package/dist/cli/commands/worktrain-daemon.d.ts +31 -0
- package/dist/cli/commands/worktrain-daemon.js +272 -0
- package/dist/cli/commands/worktrain-spawn.js +11 -9
- package/dist/cli-worktrain.js +488 -0
- package/dist/cli.js +1 -22
- package/dist/console/standalone-console.d.ts +28 -0
- package/dist/console/standalone-console.js +142 -0
- package/dist/{console/assets/index-Cb_LO718.js → console-ui/assets/index-C1JXnwZS.js} +1 -1
- package/dist/{console → console-ui}/index.html +1 -1
- package/dist/daemon/agent-loop.d.ts +27 -0
- package/dist/daemon/agent-loop.js +39 -1
- package/dist/daemon/daemon-events.d.ts +63 -1
- package/dist/daemon/workflow-runner.d.ts +3 -2
- package/dist/daemon/workflow-runner.js +285 -46
- package/dist/infrastructure/session/HttpServer.js +133 -34
- package/dist/manifest.json +136 -104
- package/dist/mcp/handlers/v2-error-mapping.d.ts +3 -0
- package/dist/mcp/handlers/v2-error-mapping.js +2 -0
- package/dist/mcp/handlers/v2-execution/advance.js +25 -0
- package/dist/mcp/handlers/v2-execution/continue-advance.js +7 -0
- package/dist/mcp/output-schemas.d.ts +30 -30
- package/dist/mcp/transports/fatal-exit.js +4 -0
- package/dist/mcp/transports/http-entry.js +0 -5
- package/dist/mcp/transports/stdio-entry.js +24 -12
- package/dist/mcp/v2/tools.d.ts +4 -4
- package/dist/mcp-server.d.ts +0 -2
- package/dist/mcp-server.js +1 -42
- package/dist/trigger/adapters/github-poller.d.ts +44 -0
- package/dist/trigger/adapters/github-poller.js +190 -0
- package/dist/trigger/adapters/gitlab-poller.d.ts +27 -0
- package/dist/trigger/adapters/gitlab-poller.js +81 -0
- package/dist/trigger/index.d.ts +4 -1
- package/dist/trigger/index.js +5 -1
- package/dist/trigger/polled-event-store.d.ts +22 -0
- package/dist/trigger/polled-event-store.js +173 -0
- package/dist/trigger/polling-scheduler.d.ts +20 -0
- package/dist/trigger/polling-scheduler.js +249 -0
- package/dist/trigger/trigger-listener.d.ts +3 -0
- package/dist/trigger/trigger-listener.js +47 -3
- package/dist/trigger/trigger-store.js +114 -33
- package/dist/trigger/types.d.ts +17 -1
- package/dist/v2/durable-core/domain/observation-builder.d.ts +3 -0
- package/dist/v2/durable-core/domain/observation-builder.js +2 -2
- package/dist/v2/durable-core/domain/prompt-renderer.d.ts +2 -1
- package/dist/v2/durable-core/domain/prompt-renderer.js +10 -0
- package/dist/v2/durable-core/schemas/export-bundle/index.d.ts +224 -224
- package/dist/v2/durable-core/schemas/session/events.d.ts +42 -42
- package/dist/v2/durable-core/schemas/session/manifest.d.ts +6 -6
- package/dist/v2/durable-core/schemas/session/validation-event.d.ts +2 -2
- package/dist/v2/durable-core/tokens/payloads.d.ts +52 -52
- package/dist/v2/usecases/console-routes.js +3 -3
- package/dist/v2/usecases/console-service.js +185 -10
- package/dist/v2/usecases/console-types.d.ts +8 -0
- package/docs/design/bridge-removal-pr-a-candidates.md +115 -0
- package/docs/design/bridge-removal-pr-a-design-review.md +79 -0
- package/docs/design/bridge-removal-pr-a-implementation-plan.md +203 -0
- package/docs/design/daemon-conversation-logging-plan.md +98 -0
- package/docs/design/daemon-conversation-logging-review.md +55 -0
- package/docs/design/daemon-conversation-logging.md +129 -0
- package/docs/design/github-polling-adapter-design-candidates.md +226 -0
- package/docs/design/github-polling-adapter-design-review-findings.md +131 -0
- package/docs/design/github-polling-adapter-implementation-plan.md +284 -0
- package/docs/design/implementation_plan.md +192 -0
- package/docs/design/workflow-id-validation-at-startup.md +146 -0
- package/docs/design/workflow-id-validation-design-review.md +87 -0
- package/docs/design/workflow-id-validation-implementation-plan.md +185 -0
- package/docs/design/worktrain-system-prompt-report-issue-candidates.md +135 -0
- package/docs/design/worktrain-system-prompt-report-issue-design-review.md +73 -0
- package/docs/discovery/design-candidates.md +180 -0
- package/docs/discovery/design-review-findings.md +110 -0
- package/docs/discovery/wr-discovery-goal-reframing.md +303 -0
- package/docs/ideas/backlog.md +627 -0
- package/package.json +1 -1
- package/workflows/architecture-scalability-audit.json +1 -1
- package/workflows/bug-investigation.agentic.v2.json +3 -3
- package/workflows/coding-task-workflow-agentic.json +32 -32
- package/workflows/coding-task-workflow-agentic.lean.v2.json +1 -1
- package/workflows/coding-task-workflow-agentic.v2.json +7 -7
- package/workflows/mr-review-workflow.agentic.v2.json +21 -12
- package/workflows/personal-learning-materials-creation-branched.json +2 -2
- package/workflows/production-readiness-audit.json +1 -1
- package/workflows/relocation-workflow-us.json +2 -2
- package/workflows/ui-ux-design-workflow.json +14 -14
- package/workflows/workflow-for-workflows.json +3 -3
- package/workflows/workflow-for-workflows.v2.json +2 -2
- package/workflows/wr.discovery.json +59 -8
- package/dist/mcp/transports/bridge-entry.d.ts +0 -102
- package/dist/mcp/transports/bridge-entry.js +0 -454
- package/dist/mcp/transports/bridge-events.d.ts +0 -51
- package/dist/mcp/transports/bridge-events.js +0 -24
- package/dist/mcp/transports/primary-tombstone.d.ts +0 -21
- package/dist/mcp/transports/primary-tombstone.js +0 -51
- /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
|
-
? {
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
608
|
-
|
|
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
|
-
|
|
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 ??
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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,
|