@exaudeus/workrail 3.35.1 → 3.37.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/config/config-file.js +2 -0
- package/dist/console-ui/assets/{index-D7jQyCSD.js → index-o-p__sHJ.js} +1 -1
- package/dist/console-ui/index.html +1 -1
- package/dist/daemon/workflow-runner.d.ts +5 -0
- package/dist/daemon/workflow-runner.js +131 -1
- package/dist/manifest.json +39 -31
- package/dist/mcp/handlers/v2-advance-events.js +1 -1
- package/dist/mcp/handlers/v2-execution/start.d.ts +1 -0
- package/dist/mcp/handlers/v2-execution/start.js +3 -2
- package/dist/trigger/notification-service.d.ts +42 -0
- package/dist/trigger/notification-service.js +164 -0
- package/dist/trigger/trigger-listener.js +7 -1
- package/dist/trigger/trigger-router.d.ts +3 -1
- package/dist/trigger/trigger-router.js +4 -1
- package/dist/v2/durable-core/schemas/export-bundle/index.d.ts +64 -32
- package/dist/v2/durable-core/schemas/session/events.d.ts +20 -10
- package/dist/v2/durable-core/schemas/session/events.js +1 -1
- package/dist/v2/durable-core/schemas/session/gaps.d.ts +8 -8
- package/dist/v2/durable-core/schemas/session/gaps.js +1 -1
- package/docs/design/agent-behavior-patterns-discovery.md +312 -0
- package/docs/design/agent-engine-communication-discovery.md +390 -0
- package/docs/design/agent-loop-architecture-alternatives-discovery.md +531 -0
- package/docs/design/agent-loop-error-handling-contract.md +238 -0
- package/docs/design/complete-step-approach-validation-discovery.md +344 -0
- package/docs/design/daemon-stuck-detection-discovery.md +174 -0
- package/docs/design/mcp-server-disconnect-discovery.md +245 -0
- package/docs/design/mcp-server-epipe-crash.md +198 -0
- package/docs/design/notification-design-candidates.md +131 -0
- package/docs/design/notification-design-review.md +84 -0
- package/docs/design/notification-implementation-plan.md +181 -0
- package/docs/design/spawn-agent-failure-modes.md +161 -0
- package/docs/design/spawn-agent-result-handling-implementation-plan.md +186 -0
- package/docs/design/stdio-simplification-design-candidates.md +341 -0
- package/docs/design/stdio-simplification-design-review.md +93 -0
- package/docs/design/stdio-simplification-implementation-plan.md +317 -0
- package/docs/design/structured-output-tools-coexist-findings.md +288 -0
- package/docs/discovery/coordinator-script-design.md +745 -0
- package/docs/discovery/coordinator-ux-discovery.md +471 -0
- package/docs/discovery/spawn-agent-failure-modes.md +309 -0
- package/docs/discovery/workflow-selection-for-discovery-tasks.md +336 -0
- package/docs/discovery/worktrain-status-briefing.md +325 -0
- package/docs/discovery/worktrain-status-design-candidates.md +202 -0
- package/docs/discovery/worktrain-status-design-review-findings.md +86 -0
- package/docs/ideas/backlog.md +688 -1
- package/docs/ideas/daemon-structured-output-vs-tool-calls.md +344 -0
- package/docs/ideas/design-candidates-backlog-consolidation.md +85 -0
- package/docs/ideas/design-candidates-spawn-agent-task.md +178 -0
- package/docs/ideas/design-review-findings-backlog-consolidation.md +39 -0
- package/docs/ideas/design-review-findings-spawn-agent-task.md +139 -0
- package/docs/ideas/implementation_plan_backlog_consolidation.md +117 -0
- package/docs/ideas/implementation_plan_spawn_agent.md +217 -0
- package/docs/plans/authoring-doc-staleness-enforcement-candidates.md +251 -0
- package/docs/plans/authoring-doc-staleness-enforcement-review.md +99 -0
- package/docs/plans/authoring-doc-staleness-enforcement.md +463 -0
- package/package.json +1 -1
|
@@ -43,6 +43,7 @@ exports.runStartupRecovery = runStartupRecovery;
|
|
|
43
43
|
exports.makeContinueWorkflowTool = makeContinueWorkflowTool;
|
|
44
44
|
exports.makeCompleteStepTool = makeCompleteStepTool;
|
|
45
45
|
exports.makeBashTool = makeBashTool;
|
|
46
|
+
exports.makeSpawnAgentTool = makeSpawnAgentTool;
|
|
46
47
|
exports.makeReportIssueTool = makeReportIssueTool;
|
|
47
48
|
exports.buildSessionRecap = buildSessionRecap;
|
|
48
49
|
exports.buildSystemPrompt = buildSystemPrompt;
|
|
@@ -62,6 +63,7 @@ const index_js_1 = require("../mcp/handlers/v2-execution/index.js");
|
|
|
62
63
|
const v2_token_ops_js_1 = require("../mcp/handlers/v2-token-ops.js");
|
|
63
64
|
const index_js_2 = require("../v2/durable-core/ids/index.js");
|
|
64
65
|
const node_outputs_js_1 = require("../v2/projections/node-outputs.js");
|
|
66
|
+
const assert_never_js_1 = require("../runtime/assert-never.js");
|
|
65
67
|
const execAsync = (0, node_util_1.promisify)(node_child_process_1.exec);
|
|
66
68
|
const BASH_TIMEOUT_MS = 5 * 60 * 1000;
|
|
67
69
|
const MAX_SESSION_RECAP_NOTES = 3;
|
|
@@ -375,6 +377,30 @@ function getSchemas() {
|
|
|
375
377
|
},
|
|
376
378
|
required: ['filePath', 'content'],
|
|
377
379
|
},
|
|
380
|
+
SpawnAgentParams: {
|
|
381
|
+
type: 'object',
|
|
382
|
+
properties: {
|
|
383
|
+
workflowId: {
|
|
384
|
+
type: 'string',
|
|
385
|
+
description: 'ID of the workflow to run in the child session (e.g. "wr.discovery").',
|
|
386
|
+
},
|
|
387
|
+
goal: {
|
|
388
|
+
type: 'string',
|
|
389
|
+
description: 'One-sentence description of what the child session should accomplish.',
|
|
390
|
+
},
|
|
391
|
+
workspacePath: {
|
|
392
|
+
type: 'string',
|
|
393
|
+
description: 'Absolute path to the workspace directory for the child session.',
|
|
394
|
+
},
|
|
395
|
+
context: {
|
|
396
|
+
type: 'object',
|
|
397
|
+
additionalProperties: true,
|
|
398
|
+
description: 'Optional initial context variables to pass to the child workflow.',
|
|
399
|
+
},
|
|
400
|
+
},
|
|
401
|
+
required: ['workflowId', 'goal', 'workspacePath'],
|
|
402
|
+
additionalProperties: false,
|
|
403
|
+
},
|
|
378
404
|
};
|
|
379
405
|
return _schemas;
|
|
380
406
|
}
|
|
@@ -657,6 +683,106 @@ function makeWriteTool(schemas, sessionId, emitter, workrailSessionId) {
|
|
|
657
683
|
},
|
|
658
684
|
};
|
|
659
685
|
}
|
|
686
|
+
function makeSpawnAgentTool(sessionId, ctx, apiKey, thisWorkrailSessionId, currentDepth, maxDepth, runWorkflowFn, schemas, emitter) {
|
|
687
|
+
return {
|
|
688
|
+
name: 'spawn_agent',
|
|
689
|
+
description: 'Spawn a child WorkRail session to handle a delegated sub-task. ' +
|
|
690
|
+
'Blocks until the child session completes, then returns the child\'s outcome and notes. ' +
|
|
691
|
+
'Use this when a step requires delegating a well-defined sub-task to a separate workflow. ' +
|
|
692
|
+
'IMPORTANT: The parent session\'s time limit (maxSessionMinutes) keeps ticking while the child runs. ' +
|
|
693
|
+
'Configure the parent with enough time to cover both its own work and the child\'s work. ' +
|
|
694
|
+
'Returns: { childSessionId, outcome: "success"|"error"|"timeout", notes: string }. ' +
|
|
695
|
+
'Check outcome before using notes -- on error/timeout, notes contains the error message.',
|
|
696
|
+
inputSchema: schemas['SpawnAgentParams'],
|
|
697
|
+
label: 'Spawn Agent',
|
|
698
|
+
execute: async (_toolCallId, params) => {
|
|
699
|
+
console.log(`[WorkflowRunner] Tool: spawn_agent sessionId=${sessionId} workflowId=${String(params.workflowId)} depth=${currentDepth}/${maxDepth}`);
|
|
700
|
+
emitter?.emit({ kind: 'tool_called', sessionId, toolName: 'spawn_agent', summary: `${String(params.workflowId)} depth=${currentDepth}`, ...withWorkrailSession(thisWorkrailSessionId) });
|
|
701
|
+
if (currentDepth >= maxDepth) {
|
|
702
|
+
const limitResult = {
|
|
703
|
+
childSessionId: null,
|
|
704
|
+
outcome: 'error',
|
|
705
|
+
notes: `Max spawn depth exceeded (currentDepth=${currentDepth}, maxDepth=${maxDepth}). ` +
|
|
706
|
+
`Cannot spawn a child session from this depth. ` +
|
|
707
|
+
`Increase agentConfig.maxSubagentDepth if deeper delegation is intentional.`,
|
|
708
|
+
};
|
|
709
|
+
return {
|
|
710
|
+
content: [{ type: 'text', text: JSON.stringify(limitResult) }],
|
|
711
|
+
details: limitResult,
|
|
712
|
+
};
|
|
713
|
+
}
|
|
714
|
+
const startInput = {
|
|
715
|
+
workflowId: String(params.workflowId),
|
|
716
|
+
workspacePath: String(params.workspacePath),
|
|
717
|
+
goal: String(params.goal),
|
|
718
|
+
};
|
|
719
|
+
const startResult = await (0, start_js_1.executeStartWorkflow)(startInput, ctx, { is_autonomous: 'true', workspacePath: String(params.workspacePath), parentSessionId: thisWorkrailSessionId });
|
|
720
|
+
if (startResult.isErr()) {
|
|
721
|
+
const errResult = {
|
|
722
|
+
childSessionId: null,
|
|
723
|
+
outcome: 'error',
|
|
724
|
+
notes: `Failed to start child workflow: ${startResult.error.kind} -- ${JSON.stringify(startResult.error)}`,
|
|
725
|
+
};
|
|
726
|
+
return {
|
|
727
|
+
content: [{ type: 'text', text: JSON.stringify(errResult) }],
|
|
728
|
+
details: errResult,
|
|
729
|
+
};
|
|
730
|
+
}
|
|
731
|
+
let childSessionId = null;
|
|
732
|
+
const childContinueToken = startResult.value.response.continueToken ?? '';
|
|
733
|
+
if (childContinueToken) {
|
|
734
|
+
const decoded = await (0, v2_token_ops_js_1.parseContinueTokenOrFail)(childContinueToken, ctx.v2.tokenCodecPorts, ctx.v2.tokenAliasStore);
|
|
735
|
+
if (decoded.isOk()) {
|
|
736
|
+
childSessionId = decoded.value.sessionId;
|
|
737
|
+
}
|
|
738
|
+
else {
|
|
739
|
+
console.warn(`[WorkflowRunner] spawn_agent: could not decode childSessionId from continueToken -- ` +
|
|
740
|
+
`childSessionId will be null in result. Reason: ${decoded.error.message}`);
|
|
741
|
+
}
|
|
742
|
+
}
|
|
743
|
+
const childResult = await runWorkflowFn({
|
|
744
|
+
workflowId: String(params.workflowId),
|
|
745
|
+
goal: String(params.goal),
|
|
746
|
+
workspacePath: String(params.workspacePath),
|
|
747
|
+
context: params.context,
|
|
748
|
+
spawnDepth: currentDepth + 1,
|
|
749
|
+
parentSessionId: thisWorkrailSessionId,
|
|
750
|
+
_preAllocatedStartResponse: startResult.value.response,
|
|
751
|
+
}, ctx, apiKey, undefined, emitter);
|
|
752
|
+
let resultObj;
|
|
753
|
+
if (childResult._tag === 'success') {
|
|
754
|
+
resultObj = {
|
|
755
|
+
childSessionId,
|
|
756
|
+
outcome: 'success',
|
|
757
|
+
notes: childResult.lastStepNotes ?? '(no notes from child session)',
|
|
758
|
+
};
|
|
759
|
+
}
|
|
760
|
+
else if (childResult._tag === 'error') {
|
|
761
|
+
resultObj = {
|
|
762
|
+
childSessionId,
|
|
763
|
+
outcome: 'error',
|
|
764
|
+
notes: childResult.message,
|
|
765
|
+
};
|
|
766
|
+
}
|
|
767
|
+
else if (childResult._tag === 'timeout') {
|
|
768
|
+
resultObj = {
|
|
769
|
+
childSessionId,
|
|
770
|
+
outcome: 'timeout',
|
|
771
|
+
notes: childResult.message,
|
|
772
|
+
};
|
|
773
|
+
}
|
|
774
|
+
else {
|
|
775
|
+
(0, assert_never_js_1.assertNever)(childResult);
|
|
776
|
+
}
|
|
777
|
+
console.log(`[WorkflowRunner] spawn_agent completed: sessionId=${sessionId} childSessionId=${childSessionId ?? 'null'} outcome=${resultObj.outcome}`);
|
|
778
|
+
emitter?.emit({ kind: 'tool_called', sessionId, toolName: 'spawn_agent_complete', summary: `outcome=${resultObj.outcome} child=${childSessionId ?? 'null'}`, ...withWorkrailSession(thisWorkrailSessionId) });
|
|
779
|
+
return {
|
|
780
|
+
content: [{ type: 'text', text: JSON.stringify(resultObj) }],
|
|
781
|
+
details: resultObj,
|
|
782
|
+
};
|
|
783
|
+
},
|
|
784
|
+
};
|
|
785
|
+
}
|
|
660
786
|
async function appendIssueAsync(issuesDir, sessionId, record) {
|
|
661
787
|
await fs.mkdir(issuesDir, { recursive: true });
|
|
662
788
|
const filePath = path.join(issuesDir, `${sessionId}.jsonl`);
|
|
@@ -768,11 +894,12 @@ Good pattern: "Question: Should I check the middleware? Answer: The workflow ste
|
|
|
768
894
|
|
|
769
895
|
## Your tools
|
|
770
896
|
- \`complete_step\`: Mark the current step complete and advance to the next one. Call this after completing ALL work required by the step. Include your notes (min 50 characters) in the notes field. The daemon manages the session token internally -- you do NOT need a continueToken. This is the preferred advancement tool for daemon sessions.
|
|
771
|
-
- \`continue_workflow\`: [DEPRECATED -- use complete_step instead
|
|
897
|
+
- \`continue_workflow\`: [DEPRECATED -- use complete_step instead. Do NOT pass a continueToken.] Only use this if complete_step is unavailable.
|
|
772
898
|
- \`Bash\`: Run shell commands. Use for building, testing, running scripts.
|
|
773
899
|
- \`Read\`: Read files.
|
|
774
900
|
- \`Write\`: Write files.
|
|
775
901
|
- \`report_issue\`: Record a structured issue, error, or unexpected behavior. Call this AND complete_step (unless fatal). Does not stop the session -- it creates a record for the auto-fix coordinator.
|
|
902
|
+
- \`spawn_agent\`: Delegate a sub-task to a child WorkRail session. BLOCKS until the child completes. Returns \`{ childSessionId, outcome: "success"|"error"|"timeout", notes: string }\`. Always check \`outcome\` before using \`notes\`. IMPORTANT: your session's time limit (maxSessionMinutes) keeps running while the child executes -- ensure your parent session has enough time for both your work AND the child's work. Maximum spawn depth is 3 by default (configurable). Use only when a step explicitly asks for delegation or when a clearly separable sub-task would benefit from its own WorkRail audit trail.
|
|
776
903
|
|
|
777
904
|
## Execution contract
|
|
778
905
|
1. Read the step carefully. Do ALL the work the step asks for.
|
|
@@ -936,6 +1063,8 @@ async function runWorkflow(trigger, ctx, apiKey, daemonRegistry, emitter) {
|
|
|
936
1063
|
return { _tag: 'success', workflowId: trigger.workflowId, stopReason: 'stop' };
|
|
937
1064
|
}
|
|
938
1065
|
const schemas = getSchemas();
|
|
1066
|
+
const spawnCurrentDepth = trigger.spawnDepth ?? 0;
|
|
1067
|
+
const spawnMaxDepth = trigger.agentConfig?.maxSubagentDepth ?? 3;
|
|
939
1068
|
const tools = [
|
|
940
1069
|
makeCompleteStepTool(sessionId, ctx, () => currentContinueToken, onAdvance, onComplete, (t) => { currentContinueToken = t; }, schemas, index_js_1.executeContinueWorkflow, emitter, workrailSessionId),
|
|
941
1070
|
makeContinueWorkflowTool(sessionId, ctx, onAdvance, onComplete, schemas, index_js_1.executeContinueWorkflow, emitter, workrailSessionId),
|
|
@@ -947,6 +1076,7 @@ async function runWorkflow(trigger, ctx, apiKey, daemonRegistry, emitter) {
|
|
|
947
1076
|
issueSummaries.push(summary);
|
|
948
1077
|
}
|
|
949
1078
|
}),
|
|
1079
|
+
makeSpawnAgentTool(sessionId, ctx, apiKey, workrailSessionId ?? '', spawnCurrentDepth, spawnMaxDepth, runWorkflow, schemas, emitter),
|
|
950
1080
|
];
|
|
951
1081
|
const [soulContent, workspaceContext, sessionNotes] = await Promise.all([
|
|
952
1082
|
loadDaemonSoul(trigger.soulFile),
|
package/dist/manifest.json
CHANGED
|
@@ -430,8 +430,8 @@
|
|
|
430
430
|
"bytes": 506
|
|
431
431
|
},
|
|
432
432
|
"config/config-file.js": {
|
|
433
|
-
"sha256": "
|
|
434
|
-
"bytes":
|
|
433
|
+
"sha256": "22006b77ef2c6094c86b97007371b96fbd8792accec652c49bdcccaa40ad327f",
|
|
434
|
+
"bytes": 7277
|
|
435
435
|
},
|
|
436
436
|
"config/feature-flags.d.ts": {
|
|
437
437
|
"sha256": "49cdf81a9c4f31eca560af5257c569143d2138ec996468b949f9807b7ad7802e",
|
|
@@ -445,12 +445,12 @@
|
|
|
445
445
|
"sha256": "cf9d09641f1c31fffe6c7835b30bbbad52572befec1acab7fb9a0c188431af36",
|
|
446
446
|
"bytes": 60355
|
|
447
447
|
},
|
|
448
|
-
"console-ui/assets/index-
|
|
449
|
-
"sha256": "
|
|
448
|
+
"console-ui/assets/index-o-p__sHJ.js": {
|
|
449
|
+
"sha256": "aa021484cd5141eb2d76c22eaed98462b60d0893980aacc0de3c27e7622771c8",
|
|
450
450
|
"bytes": 754955
|
|
451
451
|
},
|
|
452
452
|
"console-ui/index.html": {
|
|
453
|
-
"sha256": "
|
|
453
|
+
"sha256": "f893a833024bf953eb40aa9474c648cdddc4171fb73be8de5e5836898c2a383d",
|
|
454
454
|
"bytes": 417
|
|
455
455
|
},
|
|
456
456
|
"console/standalone-console.d.ts": {
|
|
@@ -502,12 +502,12 @@
|
|
|
502
502
|
"bytes": 1009
|
|
503
503
|
},
|
|
504
504
|
"daemon/workflow-runner.d.ts": {
|
|
505
|
-
"sha256": "
|
|
506
|
-
"bytes":
|
|
505
|
+
"sha256": "d62587e9c7da974ff986d2d9cb67f0b30f7f3cb98a469cf98daf1d6fd16fa897",
|
|
506
|
+
"bytes": 4593
|
|
507
507
|
},
|
|
508
508
|
"daemon/workflow-runner.js": {
|
|
509
|
-
"sha256": "
|
|
510
|
-
"bytes":
|
|
509
|
+
"sha256": "e3784aa04ead526de3ac9103d40967b32e3095ae3021ac13237034290be4ba4c",
|
|
510
|
+
"bytes": 63597
|
|
511
511
|
},
|
|
512
512
|
"di/container.d.ts": {
|
|
513
513
|
"sha256": "003bb7fb7478d627524b9b1e76bd0a963a243794a687ff233b96dc0e33a06d9f",
|
|
@@ -954,8 +954,8 @@
|
|
|
954
954
|
"bytes": 2769
|
|
955
955
|
},
|
|
956
956
|
"mcp/handlers/v2-advance-events.js": {
|
|
957
|
-
"sha256": "
|
|
958
|
-
"bytes":
|
|
957
|
+
"sha256": "c23df725685ee2062f44e05512ee4463c9b29ed4d67a4e72846496d59920733c",
|
|
958
|
+
"bytes": 5235
|
|
959
959
|
},
|
|
960
960
|
"mcp/handlers/v2-checkpoint.d.ts": {
|
|
961
961
|
"sha256": "8f22b341bb0ffffb3b24a89067e2a6513ef004ca21c1a42ce48979c2c663b18c",
|
|
@@ -1038,12 +1038,12 @@
|
|
|
1038
1038
|
"bytes": 11397
|
|
1039
1039
|
},
|
|
1040
1040
|
"mcp/handlers/v2-execution/start.d.ts": {
|
|
1041
|
-
"sha256": "
|
|
1042
|
-
"bytes":
|
|
1041
|
+
"sha256": "1ebd5c3790f5bedaf032e4f5271a78d43629b82ecc351c3fc0f17f326d915d5d",
|
|
1042
|
+
"bytes": 3558
|
|
1043
1043
|
},
|
|
1044
1044
|
"mcp/handlers/v2-execution/start.js": {
|
|
1045
|
-
"sha256": "
|
|
1046
|
-
"bytes":
|
|
1045
|
+
"sha256": "d2ffbf775d5ea6ce68174d428129fad1377033e09fcdeb69c24c9d8fb04885b4",
|
|
1046
|
+
"bytes": 21354
|
|
1047
1047
|
},
|
|
1048
1048
|
"mcp/handlers/v2-execution/workflow-object-cache.d.ts": {
|
|
1049
1049
|
"sha256": "7e58a2a020fd8443821dbe4e6a2702a9882c517f032a340c1b393cdebf4af907",
|
|
@@ -1557,6 +1557,14 @@
|
|
|
1557
1557
|
"sha256": "b8668c607788d560b38cf203750395e84eaa3164fff5711cac8f87f469714592",
|
|
1558
1558
|
"bytes": 1222
|
|
1559
1559
|
},
|
|
1560
|
+
"trigger/notification-service.d.ts": {
|
|
1561
|
+
"sha256": "c78406d3748953548f7879df8ac60cecd5e42f2f3b283f777343168ce2470b8d",
|
|
1562
|
+
"bytes": 1572
|
|
1563
|
+
},
|
|
1564
|
+
"trigger/notification-service.js": {
|
|
1565
|
+
"sha256": "693f617adc30b3a4fcebeca6a78b0da1c58819001660c017a4d0901652d675b8",
|
|
1566
|
+
"bytes": 6373
|
|
1567
|
+
},
|
|
1560
1568
|
"trigger/polled-event-store.d.ts": {
|
|
1561
1569
|
"sha256": "2952a25804177b2389d4273bfc41192477d100bc26100683861dedf28520dec1",
|
|
1562
1570
|
"bytes": 1011
|
|
@@ -1578,16 +1586,16 @@
|
|
|
1578
1586
|
"bytes": 1529
|
|
1579
1587
|
},
|
|
1580
1588
|
"trigger/trigger-listener.js": {
|
|
1581
|
-
"sha256": "
|
|
1582
|
-
"bytes":
|
|
1589
|
+
"sha256": "23f1eed165ae7ec03b2c46ff6d6fdf46f631319d5d58d3a993f710d2732e41f1",
|
|
1590
|
+
"bytes": 10585
|
|
1583
1591
|
},
|
|
1584
1592
|
"trigger/trigger-router.d.ts": {
|
|
1585
|
-
"sha256": "
|
|
1586
|
-
"bytes":
|
|
1593
|
+
"sha256": "5293a744ac4763380716ec7c0b31f16531b9a666d08a3524c6c7993486a728b6",
|
|
1594
|
+
"bytes": 2010
|
|
1587
1595
|
},
|
|
1588
1596
|
"trigger/trigger-router.js": {
|
|
1589
|
-
"sha256": "
|
|
1590
|
-
"bytes":
|
|
1597
|
+
"sha256": "e7b620d2b23a5e74f6fd3b5a39a5299d19d38f745401c545277c399336ef5eaf",
|
|
1598
|
+
"bytes": 15565
|
|
1591
1599
|
},
|
|
1592
1600
|
"trigger/trigger-store.d.ts": {
|
|
1593
1601
|
"sha256": "7afb05127d55bc3757a550dd15d4b797766b3fff29d1bfe76b303764b93322e7",
|
|
@@ -2166,8 +2174,8 @@
|
|
|
2166
2174
|
"bytes": 3397
|
|
2167
2175
|
},
|
|
2168
2176
|
"v2/durable-core/schemas/export-bundle/index.d.ts": {
|
|
2169
|
-
"sha256": "
|
|
2170
|
-
"bytes":
|
|
2177
|
+
"sha256": "fa406033adbb001b8044836a96c999273814d14b35e4c6d98b644ab22424d3d9",
|
|
2178
|
+
"bytes": 535324
|
|
2171
2179
|
},
|
|
2172
2180
|
"v2/durable-core/schemas/export-bundle/index.js": {
|
|
2173
2181
|
"sha256": "6e3566b2d05ea6302bbf4d311b8ec3e94725a8523834efe7670a79e7bd7dc40d",
|
|
@@ -2222,20 +2230,20 @@
|
|
|
2222
2230
|
"bytes": 2138
|
|
2223
2231
|
},
|
|
2224
2232
|
"v2/durable-core/schemas/session/events.d.ts": {
|
|
2225
|
-
"sha256": "
|
|
2226
|
-
"bytes":
|
|
2233
|
+
"sha256": "dc0098d909c240bccd9508c6ed987b50b6a9c162f1f9f7a1d49d53dfefc1535f",
|
|
2234
|
+
"bytes": 80635
|
|
2227
2235
|
},
|
|
2228
2236
|
"v2/durable-core/schemas/session/events.js": {
|
|
2229
|
-
"sha256": "
|
|
2230
|
-
"bytes":
|
|
2237
|
+
"sha256": "8af751c61d8c30802ce13174020893c8dfd59fcb4d15a34efc1abe05a2116e0d",
|
|
2238
|
+
"bytes": 12950
|
|
2231
2239
|
},
|
|
2232
2240
|
"v2/durable-core/schemas/session/gaps.d.ts": {
|
|
2233
|
-
"sha256": "
|
|
2234
|
-
"bytes":
|
|
2241
|
+
"sha256": "c42f2b86dd8275f5e35c8b144d5f49775741612b8625806b1aebeeb594248338",
|
|
2242
|
+
"bytes": 8983
|
|
2235
2243
|
},
|
|
2236
2244
|
"v2/durable-core/schemas/session/gaps.js": {
|
|
2237
|
-
"sha256": "
|
|
2238
|
-
"bytes":
|
|
2245
|
+
"sha256": "566d96b60855e4d62360d8ecf0058b810fd514e2be1249221b2bd2cc3e7490d0",
|
|
2246
|
+
"bytes": 2101
|
|
2239
2247
|
},
|
|
2240
2248
|
"v2/durable-core/schemas/session/index.d.ts": {
|
|
2241
2249
|
"sha256": "f4f500d33d212760f480d91fafd4474f7b12f9239a6c5e9c2d80d0fe96207b65",
|
|
@@ -46,6 +46,7 @@ export declare function buildInitialEvents(args: {
|
|
|
46
46
|
};
|
|
47
47
|
readonly goal: string;
|
|
48
48
|
readonly extraContext?: Readonly<Record<string, string>>;
|
|
49
|
+
readonly parentSessionId?: string;
|
|
49
50
|
}): readonly DomainEventV1[];
|
|
50
51
|
export declare function mintStartTokens(args: {
|
|
51
52
|
readonly sessionId: SessionId;
|
|
@@ -109,7 +109,7 @@ function loadAndPinWorkflow(args) {
|
|
|
109
109
|
});
|
|
110
110
|
}
|
|
111
111
|
function buildInitialEvents(args) {
|
|
112
|
-
const { sessionId, runId, nodeId, workflowId, workflowHash, workflowSourceKind, workflowSourceRef, snapshotRef, observations, idFactory, goal, extraContext, } = args;
|
|
112
|
+
const { sessionId, runId, nodeId, workflowId, workflowHash, workflowSourceKind, workflowSourceRef, snapshotRef, observations, idFactory, goal, extraContext, parentSessionId, } = args;
|
|
113
113
|
const evtSessionCreated = idFactory.mintEventId();
|
|
114
114
|
const evtRunStarted = idFactory.mintEventId();
|
|
115
115
|
const evtNodeCreated = idFactory.mintEventId();
|
|
@@ -123,7 +123,7 @@ function buildInitialEvents(args) {
|
|
|
123
123
|
sessionId,
|
|
124
124
|
kind: constants_js_1.EVENT_KIND.SESSION_CREATED,
|
|
125
125
|
dedupeKey: `session_created:${sessionId}`,
|
|
126
|
-
data: {},
|
|
126
|
+
data: parentSessionId !== undefined ? { parentSessionId } : {},
|
|
127
127
|
},
|
|
128
128
|
{
|
|
129
129
|
v: 1,
|
|
@@ -314,6 +314,7 @@ function executeStartWorkflow(input, ctx, internalContext) {
|
|
|
314
314
|
idFactory,
|
|
315
315
|
goal: input.goal,
|
|
316
316
|
extraContext: internalContext,
|
|
317
|
+
parentSessionId: internalContext?.['parentSessionId'],
|
|
317
318
|
});
|
|
318
319
|
const emptyTruth = { manifest: [], events: [] };
|
|
319
320
|
return gate.withHealthySessionLock(sessionId, (lock) => sessionStore.append(lock, {
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import type { WorkflowRunResult } from '../daemon/workflow-runner.js';
|
|
2
|
+
export type ExecFileNotifyFn = (file: string, args: readonly string[], options: {
|
|
3
|
+
timeout: number;
|
|
4
|
+
}, callback: (error: Error | null) => void) => void;
|
|
5
|
+
export type FetchNotifyFn = (url: string, init: {
|
|
6
|
+
method: string;
|
|
7
|
+
headers: Record<string, string>;
|
|
8
|
+
body: string;
|
|
9
|
+
signal: AbortSignal;
|
|
10
|
+
}) => Promise<{
|
|
11
|
+
ok: boolean;
|
|
12
|
+
status: number;
|
|
13
|
+
}>;
|
|
14
|
+
export interface NotificationConfig {
|
|
15
|
+
readonly macOs: boolean;
|
|
16
|
+
readonly webhookUrl?: string;
|
|
17
|
+
readonly execFileFn?: ExecFileNotifyFn;
|
|
18
|
+
readonly fetchFn?: FetchNotifyFn;
|
|
19
|
+
readonly platformFn?: () => NodeJS.Platform;
|
|
20
|
+
}
|
|
21
|
+
export interface NotificationPayload {
|
|
22
|
+
readonly event: 'session_completed';
|
|
23
|
+
readonly workflowId: string;
|
|
24
|
+
readonly outcome: 'success' | 'error' | 'timeout' | 'delivery_failed';
|
|
25
|
+
readonly detail: string;
|
|
26
|
+
readonly goal: string;
|
|
27
|
+
readonly timestamp: string;
|
|
28
|
+
}
|
|
29
|
+
export declare function buildNotificationBody(result: WorkflowRunResult, goal: string): string;
|
|
30
|
+
export declare function buildOutcome(result: WorkflowRunResult): NotificationPayload['outcome'];
|
|
31
|
+
export declare function buildDetail(result: WorkflowRunResult): string;
|
|
32
|
+
export declare class NotificationService {
|
|
33
|
+
private readonly _macOsEnabled;
|
|
34
|
+
private readonly _webhookUrl;
|
|
35
|
+
private readonly _execFileFn;
|
|
36
|
+
private readonly _fetchFn;
|
|
37
|
+
constructor(config: NotificationConfig);
|
|
38
|
+
notify(result: WorkflowRunResult, goal: string): void;
|
|
39
|
+
private _doNotify;
|
|
40
|
+
private _notifyMacOs;
|
|
41
|
+
private _notifyWebhook;
|
|
42
|
+
}
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.NotificationService = void 0;
|
|
37
|
+
exports.buildNotificationBody = buildNotificationBody;
|
|
38
|
+
exports.buildOutcome = buildOutcome;
|
|
39
|
+
exports.buildDetail = buildDetail;
|
|
40
|
+
const childProcess = __importStar(require("node:child_process"));
|
|
41
|
+
const os = __importStar(require("node:os"));
|
|
42
|
+
function buildNotificationBody(result, goal) {
|
|
43
|
+
const truncated = goal.length > 60 ? `${goal.slice(0, 57)}...` : goal;
|
|
44
|
+
switch (result._tag) {
|
|
45
|
+
case 'success':
|
|
46
|
+
return `Session completed: ${truncated}`;
|
|
47
|
+
case 'error':
|
|
48
|
+
return `Session failed: ${truncated}`;
|
|
49
|
+
case 'timeout':
|
|
50
|
+
return `Session timed out: ${truncated}`;
|
|
51
|
+
case 'delivery_failed':
|
|
52
|
+
return `Session completed but result delivery failed: ${truncated}`;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
function buildOutcome(result) {
|
|
56
|
+
return result._tag;
|
|
57
|
+
}
|
|
58
|
+
function buildDetail(result) {
|
|
59
|
+
switch (result._tag) {
|
|
60
|
+
case 'success':
|
|
61
|
+
return `stopReason: ${result.stopReason}`;
|
|
62
|
+
case 'error':
|
|
63
|
+
return result.message;
|
|
64
|
+
case 'timeout':
|
|
65
|
+
return result.message;
|
|
66
|
+
case 'delivery_failed':
|
|
67
|
+
return `stopReason: ${result.stopReason}; deliveryError: ${result.deliveryError}`;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
class NotificationService {
|
|
71
|
+
constructor(config) {
|
|
72
|
+
const getPlatform = config.platformFn ?? os.platform.bind(os);
|
|
73
|
+
if (config.macOs && getPlatform() !== 'darwin') {
|
|
74
|
+
console.warn('[NotificationService] WORKTRAIN_NOTIFY_MACOS=true but platform is not darwin ' +
|
|
75
|
+
`(platform: ${getPlatform()}). macOS notifications are disabled.`);
|
|
76
|
+
this._macOsEnabled = false;
|
|
77
|
+
}
|
|
78
|
+
else {
|
|
79
|
+
this._macOsEnabled = config.macOs;
|
|
80
|
+
}
|
|
81
|
+
if (config.webhookUrl !== undefined && config.webhookUrl !== '') {
|
|
82
|
+
let valid = false;
|
|
83
|
+
try {
|
|
84
|
+
const parsed = new URL(config.webhookUrl);
|
|
85
|
+
valid = parsed.protocol === 'http:' || parsed.protocol === 'https:';
|
|
86
|
+
}
|
|
87
|
+
catch {
|
|
88
|
+
valid = false;
|
|
89
|
+
}
|
|
90
|
+
if (!valid) {
|
|
91
|
+
console.warn(`[NotificationService] WORKTRAIN_NOTIFY_WEBHOOK is not a valid http(s) URL ` +
|
|
92
|
+
`("${config.webhookUrl}"). Webhook notifications are disabled.`);
|
|
93
|
+
this._webhookUrl = undefined;
|
|
94
|
+
}
|
|
95
|
+
else {
|
|
96
|
+
this._webhookUrl = config.webhookUrl;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
else {
|
|
100
|
+
this._webhookUrl = undefined;
|
|
101
|
+
}
|
|
102
|
+
this._execFileFn = config.execFileFn ?? ((file, args, options, callback) => {
|
|
103
|
+
childProcess.execFile(file, args, options, callback);
|
|
104
|
+
});
|
|
105
|
+
this._fetchFn = config.fetchFn ?? ((url, init) => globalThis.fetch(url, init));
|
|
106
|
+
}
|
|
107
|
+
notify(result, goal) {
|
|
108
|
+
void this._doNotify(result, goal).catch(() => {
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
async _doNotify(result, goal) {
|
|
112
|
+
const body = buildNotificationBody(result, goal);
|
|
113
|
+
const deliveries = [];
|
|
114
|
+
if (this._macOsEnabled) {
|
|
115
|
+
deliveries.push(this._notifyMacOs(body, result.workflowId));
|
|
116
|
+
}
|
|
117
|
+
if (this._webhookUrl !== undefined) {
|
|
118
|
+
deliveries.push(this._notifyWebhook(result, goal));
|
|
119
|
+
}
|
|
120
|
+
await Promise.allSettled(deliveries);
|
|
121
|
+
}
|
|
122
|
+
_notifyMacOs(body, workflowId) {
|
|
123
|
+
const script = `display notification ${JSON.stringify(body)} with title "WorkTrain" subtitle ${JSON.stringify(workflowId)}`;
|
|
124
|
+
return new Promise((resolve) => {
|
|
125
|
+
this._execFileFn('osascript', ['-e', script], { timeout: 5000 }, (error) => {
|
|
126
|
+
if (error) {
|
|
127
|
+
console.warn(`[NotificationService] macOS notification failed: ${error.message}`);
|
|
128
|
+
}
|
|
129
|
+
resolve();
|
|
130
|
+
});
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
async _notifyWebhook(result, goal) {
|
|
134
|
+
const url = this._webhookUrl;
|
|
135
|
+
const payload = {
|
|
136
|
+
event: 'session_completed',
|
|
137
|
+
workflowId: result.workflowId,
|
|
138
|
+
outcome: buildOutcome(result),
|
|
139
|
+
detail: buildDetail(result),
|
|
140
|
+
goal,
|
|
141
|
+
timestamp: new Date().toISOString(),
|
|
142
|
+
};
|
|
143
|
+
const controller = new AbortController();
|
|
144
|
+
const timer = setTimeout(() => controller.abort(), 30000);
|
|
145
|
+
try {
|
|
146
|
+
const res = await this._fetchFn(url, {
|
|
147
|
+
method: 'POST',
|
|
148
|
+
headers: { 'Content-Type': 'application/json' },
|
|
149
|
+
body: JSON.stringify(payload),
|
|
150
|
+
signal: controller.signal,
|
|
151
|
+
});
|
|
152
|
+
if (!res.ok) {
|
|
153
|
+
console.warn(`[NotificationService] Webhook notification failed: HTTP ${res.status} from ${url}`);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
catch (e) {
|
|
157
|
+
console.warn(`[NotificationService] Webhook notification error: ${String(e)}`);
|
|
158
|
+
}
|
|
159
|
+
finally {
|
|
160
|
+
clearTimeout(timer);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
exports.NotificationService = NotificationService;
|
|
@@ -44,6 +44,7 @@ const http = __importStar(require("node:http"));
|
|
|
44
44
|
const trigger_store_js_1 = require("./trigger-store.js");
|
|
45
45
|
const trigger_router_js_1 = require("./trigger-router.js");
|
|
46
46
|
const config_file_js_1 = require("../config/config-file.js");
|
|
47
|
+
const notification_service_js_1 = require("./notification-service.js");
|
|
47
48
|
const workflow_runner_js_1 = require("../daemon/workflow-runner.js");
|
|
48
49
|
const types_js_1 = require("./types.js");
|
|
49
50
|
const polling_scheduler_js_1 = require("./polling-scheduler.js");
|
|
@@ -177,8 +178,13 @@ async function startTriggerListener(ctx, options) {
|
|
|
177
178
|
: undefined;
|
|
178
179
|
const parsed = parseInt(maxConcurrencyRaw ?? '', 10);
|
|
179
180
|
const maxConcurrentSessions = !isNaN(parsed) ? parsed : undefined;
|
|
181
|
+
const notifyMacOs = (workrailConfig.kind === 'ok' && workrailConfig.value['WORKTRAIN_NOTIFY_MACOS'] === 'true');
|
|
182
|
+
const notifyWebhook = workrailConfig.kind === 'ok' ? workrailConfig.value['WORKTRAIN_NOTIFY_WEBHOOK'] : undefined;
|
|
183
|
+
const notificationService = (notifyMacOs || (notifyWebhook !== undefined && notifyWebhook !== ''))
|
|
184
|
+
? new notification_service_js_1.NotificationService({ macOs: notifyMacOs, webhookUrl: notifyWebhook })
|
|
185
|
+
: undefined;
|
|
180
186
|
const runWorkflowFn = options.runWorkflowFn ?? workflow_runner_js_1.runWorkflow;
|
|
181
|
-
const router = new trigger_router_js_1.TriggerRouter(triggerIndex, ctx, apiKey, runWorkflowFn, undefined, maxConcurrentSessions, options.emitter);
|
|
187
|
+
const router = new trigger_router_js_1.TriggerRouter(triggerIndex, ctx, apiKey, runWorkflowFn, undefined, maxConcurrentSessions, options.emitter, notificationService);
|
|
182
188
|
const app = createTriggerApp(router);
|
|
183
189
|
const allTriggers = [...triggerIndex.values()];
|
|
184
190
|
const polledEventStore = new polled_event_store_js_1.PolledEventStore(env);
|
|
@@ -3,6 +3,7 @@ import type { V2ToolContext } from '../mcp/types.js';
|
|
|
3
3
|
import type { TriggerDefinition, WebhookEvent } from './types.js';
|
|
4
4
|
import type { ExecFn } from './delivery-action.js';
|
|
5
5
|
import type { DaemonEventEmitter } from '../daemon/daemon-events.js';
|
|
6
|
+
import type { NotificationService } from './notification-service.js';
|
|
6
7
|
export type RouteError = {
|
|
7
8
|
readonly kind: 'not_found';
|
|
8
9
|
readonly triggerId: string;
|
|
@@ -31,7 +32,8 @@ export declare class TriggerRouter {
|
|
|
31
32
|
private readonly semaphore;
|
|
32
33
|
private readonly _maxConcurrentSessions;
|
|
33
34
|
private readonly emitter;
|
|
34
|
-
|
|
35
|
+
private readonly notificationService;
|
|
36
|
+
constructor(index: ReadonlyMap<string, TriggerDefinition>, ctx: V2ToolContext, apiKey: string, runWorkflowFn: RunWorkflowFn, execFn?: ExecFn, maxConcurrentSessions?: number, emitter?: DaemonEventEmitter, notificationService?: NotificationService);
|
|
35
37
|
get activeSessions(): number;
|
|
36
38
|
get maxConcurrentSessions(): number;
|
|
37
39
|
route(event: WebhookEvent): RouteResult;
|
|
@@ -182,7 +182,7 @@ class Semaphore {
|
|
|
182
182
|
}
|
|
183
183
|
const DEFAULT_MAX_CONCURRENT_SESSIONS = 3;
|
|
184
184
|
class TriggerRouter {
|
|
185
|
-
constructor(index, ctx, apiKey, runWorkflowFn, execFn, maxConcurrentSessions, emitter) {
|
|
185
|
+
constructor(index, ctx, apiKey, runWorkflowFn, execFn, maxConcurrentSessions, emitter, notificationService) {
|
|
186
186
|
this.index = index;
|
|
187
187
|
this.ctx = ctx;
|
|
188
188
|
this.apiKey = apiKey;
|
|
@@ -190,6 +190,7 @@ class TriggerRouter {
|
|
|
190
190
|
this.queue = new index_js_1.KeyedAsyncQueue();
|
|
191
191
|
this.execFn = execFn ?? execFileAsync;
|
|
192
192
|
this.emitter = emitter;
|
|
193
|
+
this.notificationService = notificationService;
|
|
193
194
|
const requested = maxConcurrentSessions ?? DEFAULT_MAX_CONCURRENT_SESSIONS;
|
|
194
195
|
const cap = Number.isNaN(requested) ? DEFAULT_MAX_CONCURRENT_SESSIONS : requested;
|
|
195
196
|
if (cap < 1) {
|
|
@@ -301,6 +302,7 @@ class TriggerRouter {
|
|
|
301
302
|
console.log(`[TriggerRouter] Workflow failed: triggerId=${trigger.id} ` +
|
|
302
303
|
`workflowId=${trigger.workflowId} error=${result.message} stopReason=${result.stopReason}`);
|
|
303
304
|
}
|
|
305
|
+
this.notificationService?.notify(result, workflowTrigger.goal);
|
|
304
306
|
await maybeRunDelivery(trigger.id, trigger, originalResult, this.execFn);
|
|
305
307
|
});
|
|
306
308
|
return { _tag: 'enqueued', triggerId: trigger.id };
|
|
@@ -336,6 +338,7 @@ class TriggerRouter {
|
|
|
336
338
|
console.log(`[TriggerRouter] Dispatch failed: workflowId=${workflowTrigger.workflowId} ` +
|
|
337
339
|
`error=${result.message} stopReason=${result.stopReason}`);
|
|
338
340
|
}
|
|
341
|
+
this.notificationService?.notify(result, workflowTrigger.goal);
|
|
339
342
|
});
|
|
340
343
|
return workflowTrigger.workflowId;
|
|
341
344
|
}
|