@dotdrelle/wiki-manager 0.11.10 → 0.12.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (85) hide show
  1. package/README.md +32 -7
  2. package/agents.docker-compose.yml +3 -0
  3. package/docker-compose.yml +1 -0
  4. package/package.json +3 -2
  5. package/src/activity/activityAggregator.js +109 -0
  6. package/src/activity/activityAggregator.test.js +60 -0
  7. package/src/activity/activityDeduplicator.js +50 -0
  8. package/src/activity/progressCalculator.js +61 -0
  9. package/src/activity/runSynthesis.js +15 -0
  10. package/src/agent/graph.js +101 -38
  11. package/src/agent/graph.test.js +75 -5
  12. package/src/cli/wiki-manager.js +22 -0
  13. package/src/contracts/schemas.js +249 -13
  14. package/src/contracts/schemas.test.js +147 -2
  15. package/src/core/activity.js +8 -5
  16. package/src/core/agentEvents.js +260 -23
  17. package/src/core/agentEvents.test.js +53 -0
  18. package/src/core/compose.js +6 -1
  19. package/src/core/dockerCompose.test.js +12 -0
  20. package/src/core/documentIntake.js +33 -38
  21. package/src/core/documentIntake.test.js +25 -0
  22. package/src/core/env.js +11 -1
  23. package/src/core/jobQueue.js +26 -2
  24. package/src/core/mcp.js +30 -3
  25. package/src/core/mcp.test.js +69 -1
  26. package/src/core/plan.js +46 -4
  27. package/src/core/plan.test.js +16 -1
  28. package/src/core/planPatch.js +64 -3
  29. package/src/core/planPatch.test.js +110 -1
  30. package/src/core/queueStore.test.js +21 -0
  31. package/src/core/runtimeLog.js +119 -0
  32. package/src/core/runtimeLog.test.js +84 -0
  33. package/src/core/wikirc.js +22 -0
  34. package/src/core/wikirc.test.js +49 -1
  35. package/src/core/workflow.js +14 -3
  36. package/src/graph/graphAggregator.js +5 -0
  37. package/src/graph/graphPatch.js +8 -0
  38. package/src/graph/graphSnapshot.js +14 -0
  39. package/src/graph/graphVisibilityPolicy.js +40 -0
  40. package/src/graph/runGraphProjector.js +87 -0
  41. package/src/graph/runGraphProjector.test.js +111 -0
  42. package/src/orchestrator/agentRegistry.js +172 -0
  43. package/src/orchestrator/agentRegistry.test.js +114 -0
  44. package/src/orchestrator/approvalPolicy.js +126 -0
  45. package/src/orchestrator/approvalPolicy.test.js +78 -0
  46. package/src/orchestrator/assignmentManager.js +80 -0
  47. package/src/orchestrator/attemptManager.js +168 -0
  48. package/src/orchestrator/attemptManager.test.js +160 -0
  49. package/src/orchestrator/budgetManager.js +127 -0
  50. package/src/orchestrator/capabilityRegistry.js +64 -0
  51. package/src/orchestrator/capabilityRegistry.test.js +59 -0
  52. package/src/orchestrator/capabilityResolver.js +151 -0
  53. package/src/orchestrator/capabilityResolver.test.js +127 -0
  54. package/src/orchestrator/dependencyResolver.js +112 -0
  55. package/src/orchestrator/dispatcher.js +302 -0
  56. package/src/orchestrator/lockManager.js +51 -0
  57. package/src/orchestrator/planIntegrator.js +268 -0
  58. package/src/orchestrator/planIntegrator.test.js +213 -0
  59. package/src/orchestrator/planValidator.js +534 -0
  60. package/src/orchestrator/planValidator.test.js +262 -0
  61. package/src/orchestrator/resultAggregator.js +248 -0
  62. package/src/orchestrator/resultAggregator.test.js +211 -0
  63. package/src/orchestrator/scheduler.js +103 -0
  64. package/src/orchestrator/scheduler.test.js +138 -0
  65. package/src/runtime/approvals.js +130 -1
  66. package/src/runtime/controlMessages.js +43 -0
  67. package/src/runtime/controlMessages.test.js +21 -0
  68. package/src/runtime/donna-contract.test.js +296 -33
  69. package/src/runtime/recoveryManager.js +176 -0
  70. package/src/runtime/recoveryManager.test.js +162 -0
  71. package/src/runtime/runner.e2e.test.js +98 -12
  72. package/src/runtime/runner.js +261 -227
  73. package/src/runtime/runner.test.js +249 -452
  74. package/src/runtime/server.js +142 -24
  75. package/src/runtime/server.test.js +192 -62
  76. package/src/runtime/store.js +837 -2
  77. package/src/runtime/store.test.js +269 -28
  78. package/src/runtime/supervisor.js +33 -1
  79. package/src/runtime/supervisor.test.js +47 -1
  80. package/src/shell/RightPane.tsx +8 -5
  81. package/src/shell/repl.js +7 -11
  82. package/src/shell/repl.test.js +26 -5
  83. package/src/shell/tui.tsx +1 -0
  84. package/src/shell/useAgent.ts +5 -1
  85. package/src/shell/useSession.ts +75 -19
@@ -0,0 +1,151 @@
1
+ export class CapabilityUnavailableError extends Error {
2
+ constructor(capability, reason, details = {}) {
3
+ super(`Capability unavailable: ${capability} (${reason})`);
4
+ this.name = 'CapabilityUnavailableError';
5
+ this.capability = capability;
6
+ this.reason = reason;
7
+ this.details = details;
8
+ }
9
+ }
10
+
11
+ export function resolve(capability, { workspaceConfig = {}, registry } = {}) {
12
+ const capabilityId = capabilityName(capability);
13
+ if (!capabilityId) throw new CapabilityUnavailableError(capability, 'missing_capability');
14
+ if (!registry || typeof registry.providersFor !== 'function') {
15
+ throw new CapabilityUnavailableError(capabilityId, 'missing_registry');
16
+ }
17
+
18
+ const allProviders = registry.providersFor(capability) ?? [];
19
+ if (allProviders.length === 0) {
20
+ throw new CapabilityUnavailableError(capabilityId, 'capability_not_found');
21
+ }
22
+
23
+ const compatible = allProviders.filter((provider) => isCompatible(provider, registry));
24
+ if (compatible.length === 0) {
25
+ throw new CapabilityUnavailableError(capabilityId, 'contract_incompatible', { providers: providerIds(allProviders) });
26
+ }
27
+
28
+ const routing = routingForCapability(workspaceConfig, capabilityId);
29
+ const authorized = filterConfiguredAgents(compatible, routing.allowedAgents);
30
+ if (authorized.length === 0) {
31
+ throw new CapabilityUnavailableError(capabilityId, 'agent_not_allowed', {
32
+ allowedAgents: routing.allowedAgents,
33
+ providers: providerIds(compatible),
34
+ });
35
+ }
36
+
37
+ const healthy = authorized.filter(isHealthy);
38
+ if (healthy.length === 0) {
39
+ throw new CapabilityUnavailableError(capabilityId, 'agent_unhealthy', { providers: providerIds(authorized) });
40
+ }
41
+
42
+ const available = healthy.filter(isAvailable);
43
+ if (available.length === 0) {
44
+ throw new CapabilityUnavailableError(capabilityId, 'agent_unavailable', { providers: providerIds(healthy) });
45
+ }
46
+
47
+ const preferred = firstConfiguredProvider(available, routing.preferredAgents);
48
+ if (preferred) return assignment(preferred);
49
+
50
+ if (routing.preferredAgents.length > 0) {
51
+ const fallback = firstConfiguredProvider(available, routing.fallbackAgents);
52
+ if (fallback) return assignment(fallback);
53
+ throw new CapabilityUnavailableError(capabilityId, 'preferred_agent_unavailable', {
54
+ preferredAgents: routing.preferredAgents,
55
+ fallbackAgents: routing.fallbackAgents,
56
+ providers: providerIds(available),
57
+ });
58
+ }
59
+
60
+ const candidates = routing.fallbackAgents.length > 0
61
+ ? filterConfiguredAgents(available, routing.fallbackAgents)
62
+ : available;
63
+ if (candidates.length === 0) {
64
+ throw new CapabilityUnavailableError(capabilityId, 'fallback_agent_unavailable', {
65
+ fallbackAgents: routing.fallbackAgents,
66
+ providers: providerIds(available),
67
+ });
68
+ }
69
+
70
+ return assignment([...candidates].sort(compareProviderScore)[0]);
71
+ }
72
+
73
+ function assignment(provider) {
74
+ return { agentInstanceId: provider.agentInstanceId };
75
+ }
76
+
77
+ function isCompatible(provider, registry) {
78
+ const contractVersion = provider.description?.contractVersion ?? provider.contractVersion;
79
+ return typeof registry.isCompatible === 'function'
80
+ ? registry.isCompatible(contractVersion)
81
+ : String(contractVersion ?? '') === '1';
82
+ }
83
+
84
+ function routingForCapability(workspaceConfig, capabilityId) {
85
+ const raw = workspaceConfig?.capabilityRouting?.[capabilityId] ?? {};
86
+ return {
87
+ preferredAgents: stringList(raw.preferredAgents),
88
+ fallbackAgents: stringList(raw.fallbackAgents),
89
+ allowedAgents: stringList(raw.allowedAgents),
90
+ };
91
+ }
92
+
93
+ function filterConfiguredAgents(providers, configuredAgents) {
94
+ if (!Array.isArray(configuredAgents) || configuredAgents.length === 0) return providers;
95
+ const allowed = new Set(configuredAgents.map(String));
96
+ return providers.filter((provider) => allowed.has(provider.agentInstanceId));
97
+ }
98
+
99
+ function firstConfiguredProvider(providers, configuredAgents) {
100
+ if (!Array.isArray(configuredAgents) || configuredAgents.length === 0) return null;
101
+ for (const agentInstanceId of configuredAgents) {
102
+ const provider = providers.find((candidate) => candidate.agentInstanceId === agentInstanceId);
103
+ if (provider) return provider;
104
+ }
105
+ return null;
106
+ }
107
+
108
+ function isHealthy(provider) {
109
+ return ['available', 'degraded'].includes(String(provider.health ?? provider.description?.health?.status ?? ''));
110
+ }
111
+
112
+ function isAvailable(provider) {
113
+ if (provider.available === false) return false;
114
+ if (provider.availability === 'unavailable') return false;
115
+ if (provider.busy === true && provider.acceptsQueuedWork === false) return false;
116
+ return true;
117
+ }
118
+
119
+ function compareProviderScore(a, b) {
120
+ return costScore(a) - costScore(b)
121
+ || historyScore(b) - historyScore(a)
122
+ || String(a.agentInstanceId).localeCompare(String(b.agentInstanceId));
123
+ }
124
+
125
+ function costScore(provider) {
126
+ const cost = provider.capability?.estimatedCost ?? provider.estimatedCost ?? {};
127
+ const llmCalls = Number(cost.llmCalls ?? 0);
128
+ const tokenRange = Array.isArray(cost.tokenRange) ? cost.tokenRange : [];
129
+ const tokenAverage = tokenRange.length >= 2 ? (Number(tokenRange[0]) + Number(tokenRange[1])) / 2 : 0;
130
+ return (Number.isFinite(llmCalls) ? llmCalls : 0) * 100000
131
+ + (Number.isFinite(tokenAverage) ? tokenAverage : 0);
132
+ }
133
+
134
+ function historyScore(provider) {
135
+ const successRate = Number(provider.history?.successRate ?? provider.successRate ?? 0);
136
+ return Number.isFinite(successRate) ? successRate : 0;
137
+ }
138
+
139
+ function providerIds(providers) {
140
+ return providers.map((provider) => provider.agentInstanceId).filter(Boolean);
141
+ }
142
+
143
+ function stringList(value) {
144
+ return Array.isArray(value) ? value.map(String).filter(Boolean) : [];
145
+ }
146
+
147
+ function capabilityName(value) {
148
+ const raw = String(value ?? '').trim();
149
+ const index = raw.lastIndexOf('@');
150
+ return index > 0 ? raw.slice(0, index) : raw;
151
+ }
@@ -0,0 +1,127 @@
1
+ import assert from 'node:assert/strict';
2
+ import test from 'node:test';
3
+ import { CapabilityUnavailableError, resolve } from './capabilityResolver.js';
4
+
5
+ function provider(agentInstanceId, capabilityId = 'knowledge.update', {
6
+ contractVersion = '1',
7
+ health = 'available',
8
+ version = '1',
9
+ estimatedCost = null,
10
+ history = null,
11
+ available = true,
12
+ } = {}) {
13
+ return {
14
+ agentInstanceId,
15
+ health,
16
+ available,
17
+ history,
18
+ capability: {
19
+ id: capabilityId,
20
+ version,
21
+ description: capabilityId,
22
+ inputSchema: {},
23
+ outputSchema: {},
24
+ supportedOperations: ['run'],
25
+ ...(estimatedCost ? { estimatedCost } : {}),
26
+ },
27
+ description: {
28
+ contractVersion,
29
+ agentType: agentInstanceId.split('-')[0],
30
+ agentInstanceId,
31
+ displayName: agentInstanceId,
32
+ capabilities: [],
33
+ },
34
+ };
35
+ }
36
+
37
+ function registry(providers, { compatibleVersions = ['1'] } = {}) {
38
+ return {
39
+ providersFor(capability) {
40
+ const id = String(capability).split('@')[0];
41
+ return providers.filter((item) => item.capability.id === id);
42
+ },
43
+ isCompatible(contractVersion) {
44
+ return compatibleVersions.includes(String(contractVersion));
45
+ },
46
+ };
47
+ }
48
+
49
+ test('capabilityResolver rejects missing capability without falling back to a tool', () => {
50
+ assert.throws(
51
+ () => resolve('document.publish', { registry: registry([]) }),
52
+ (error) => error instanceof CapabilityUnavailableError && error.reason === 'capability_not_found',
53
+ );
54
+ });
55
+
56
+ test('capabilityResolver rejects when workspace allowedAgents excludes every provider', () => {
57
+ assert.throws(
58
+ () => resolve('knowledge.update', {
59
+ registry: registry([provider('production-main')]),
60
+ workspaceConfig: {
61
+ capabilityRouting: {
62
+ 'knowledge.update': { allowedAgents: ['documents-main'] },
63
+ },
64
+ },
65
+ }),
66
+ (error) => error instanceof CapabilityUnavailableError && error.reason === 'agent_not_allowed',
67
+ );
68
+ });
69
+
70
+ test('capabilityResolver chooses preferred healthy provider', () => {
71
+ const result = resolve('knowledge.update', {
72
+ registry: registry([provider('production-main'), provider('production-secondary')]),
73
+ workspaceConfig: {
74
+ capabilityRouting: {
75
+ 'knowledge.update': { preferredAgents: ['production-secondary'] },
76
+ },
77
+ },
78
+ });
79
+
80
+ assert.deepEqual(result, { agentInstanceId: 'production-secondary' });
81
+ });
82
+
83
+ test('capabilityResolver uses only configured fallback when preferred is unavailable', () => {
84
+ const result = resolve('knowledge.update', {
85
+ registry: registry([
86
+ provider('production-main', 'knowledge.update', { health: 'unavailable' }),
87
+ provider('production-fallback'),
88
+ provider('production-other'),
89
+ ]),
90
+ workspaceConfig: {
91
+ capabilityRouting: {
92
+ 'knowledge.update': {
93
+ preferredAgents: ['production-main'],
94
+ fallbackAgents: ['production-fallback'],
95
+ },
96
+ },
97
+ },
98
+ });
99
+
100
+ assert.deepEqual(result, { agentInstanceId: 'production-fallback' });
101
+ });
102
+
103
+ test('capabilityResolver does not fallback to an unconfigured provider when preferred is unavailable', () => {
104
+ assert.throws(
105
+ () => resolve('knowledge.update', {
106
+ registry: registry([
107
+ provider('production-main', 'knowledge.update', { health: 'unavailable' }),
108
+ provider('production-other'),
109
+ ]),
110
+ workspaceConfig: {
111
+ capabilityRouting: {
112
+ 'knowledge.update': { preferredAgents: ['production-main'] },
113
+ },
114
+ },
115
+ }),
116
+ (error) => error instanceof CapabilityUnavailableError && error.reason === 'preferred_agent_unavailable',
117
+ );
118
+ });
119
+
120
+ test('capabilityResolver rejects incompatible contract versions', () => {
121
+ assert.throws(
122
+ () => resolve('knowledge.update', {
123
+ registry: registry([provider('future-agent', 'knowledge.update', { contractVersion: '99' })]),
124
+ }),
125
+ (error) => error instanceof CapabilityUnavailableError && error.reason === 'contract_incompatible',
126
+ );
127
+ });
@@ -0,0 +1,112 @@
1
+ import { locksForTask } from './lockManager.js';
2
+ import { approvalCovered } from './approvalPolicy.js';
3
+
4
+ const DONE_STATUSES = new Set(['done', 'completed', 'complete', 'success', 'succeeded']);
5
+ const TERMINAL_STATUSES = new Set([...DONE_STATUSES, 'failed', 'cancelled', 'canceled', 'skipped']);
6
+
7
+ export function readyTasks(dag, {
8
+ registry = null,
9
+ lockManager = null,
10
+ budgetManager = null,
11
+ approvals = [],
12
+ activeTaskIds = [],
13
+ } = {}) {
14
+ const tasks = normalizeTasks(dag);
15
+ const done = new Set(tasks.filter((task) => DONE_STATUSES.has(statusOf(task))).map(taskId));
16
+ const active = new Set([...activeTaskIds].map(String));
17
+ return tasks
18
+ .filter((task) => statusOf(task) === 'pending')
19
+ .filter((task) => !active.has(taskId(task)))
20
+ .filter((task) => dependenciesDone(task, done))
21
+ .filter((task) => groupBarrierSatisfied(task, tasks))
22
+ .filter((task) => approvalCovered(task, approvals, { runId: task?.runId ?? dag?.runId ?? null, workspaceId: dag?.workspace ?? null, planRevision: dag?.planRevision ?? null }))
23
+ .filter((task) => agentSane(task, registry))
24
+ .filter((task) => locksFree(task, lockManager))
25
+ .filter((task) => budgetOk(task, budgetManager))
26
+ .sort(compareTaskPriority);
27
+ }
28
+
29
+ function normalizeTasks(dag) {
30
+ if (Array.isArray(dag)) return dag;
31
+ if (Array.isArray(dag?.tasks)) return dag.tasks;
32
+ if (Array.isArray(dag?.plan)) return dag.plan;
33
+ return [];
34
+ }
35
+
36
+ function dependenciesDone(task, done) {
37
+ return dependsOn(task).every((dep) => done.has(String(dep)));
38
+ }
39
+
40
+ function groupBarrierSatisfied(task, tasks) {
41
+ const groupId = task?.dependsOnGroup;
42
+ if (groupId == null || groupId === '') return true;
43
+ const groupTasks = tasks.filter((candidate) => taskGroupId(candidate) === String(groupId));
44
+ if (groupTasks.length === 0) return false;
45
+ return groupTasks.every((candidate) => DONE_STATUSES.has(statusOf(candidate)));
46
+ }
47
+
48
+ function agentSane(task, registry) {
49
+ if (!registry || typeof registry.providersFor !== 'function') return true;
50
+ const capability = task?.requiredCapability;
51
+ if (!capability) return false;
52
+ const providers = registry.providersFor(capability) ?? [];
53
+ return providers.some((provider) => providerSupportsTask(provider, task, registry));
54
+ }
55
+
56
+ function providerSupportsTask(provider, task, registry) {
57
+ const contractVersion = provider?.description?.contractVersion ?? provider?.contractVersion;
58
+ if (typeof registry?.isCompatible === 'function' && !registry.isCompatible(contractVersion)) return false;
59
+ const health = String(provider?.health ?? provider?.description?.health?.status ?? '');
60
+ if (!['available', 'degraded'].includes(health)) return false;
61
+ if (provider?.available === false || provider?.availability === 'unavailable') return false;
62
+ const operations = provider?.capability?.supportedOperations ?? [];
63
+ return !task?.operation || operations.length === 0 || operations.includes(task.operation);
64
+ }
65
+
66
+ function locksFree(task, lockManager) {
67
+ if (!lockManager) return true;
68
+ if (typeof lockManager.canAcquire === 'function') return lockManager.canAcquire(task);
69
+ const locked = new Set(lockManager.lockedLocks ?? lockManager.locks ?? []);
70
+ return locksForTask(task).every((lock) => !locked.has(lock));
71
+ }
72
+
73
+ function budgetOk(task, budgetManager) {
74
+ if (!budgetManager || typeof budgetManager.canStartTask !== 'function') return true;
75
+ return budgetManager.canStartTask(task);
76
+ }
77
+
78
+ function compareTaskPriority(a, b) {
79
+ return priority(a) - priority(b)
80
+ || stepNumber(a) - stepNumber(b)
81
+ || taskId(a).localeCompare(taskId(b));
82
+ }
83
+
84
+ function priority(task) {
85
+ const value = Number(task?.priority);
86
+ return Number.isFinite(value) ? value : Number.MAX_SAFE_INTEGER;
87
+ }
88
+
89
+ function stepNumber(task) {
90
+ const value = Number(task?.step);
91
+ return Number.isFinite(value) ? value : Number.MAX_SAFE_INTEGER;
92
+ }
93
+
94
+ function statusOf(task) {
95
+ return String(task?.status ?? 'pending').toLowerCase();
96
+ }
97
+
98
+ function dependsOn(task) {
99
+ return Array.isArray(task?.dependsOn) ? task.dependsOn : [];
100
+ }
101
+
102
+ function taskGroupId(task) {
103
+ return task?.groupId ?? task?.group ?? task?.taskGroupId ?? null;
104
+ }
105
+
106
+ function taskId(task) {
107
+ return String(task?.id ?? task?.step);
108
+ }
109
+
110
+ export function isTerminalTask(task) {
111
+ return TERMINAL_STATUSES.has(statusOf(task));
112
+ }
@@ -0,0 +1,302 @@
1
+ import { normalizeActivity, parseJsonText } from '../core/activity.js';
2
+ import { createAgentEvent, dispatchAgentEvent } from '../core/agentEvents.js';
3
+ import { callMcpTool, formatMcpToolResult } from '../core/mcp.js';
4
+ import { emitRuntimeLog, pollActivitiesOnce } from '../runtime/supervisor.js';
5
+
6
+ const TERMINAL_STATUSES = new Set(['done', 'failed', 'cancelled', 'canceled', 'complete', 'completed', 'success', 'error']);
7
+
8
+ export function createDispatcher({
9
+ session = null,
10
+ callTool = callMcpTool,
11
+ pollIntervalMs = 250,
12
+ } = {}) {
13
+ return {
14
+ execute(task, assignment, options = {}) {
15
+ return execute(task, assignment, {
16
+ session,
17
+ callTool,
18
+ pollIntervalMs,
19
+ ...options,
20
+ });
21
+ },
22
+ };
23
+ }
24
+
25
+ export async function execute(task, assignment, {
26
+ session,
27
+ callTool = callMcpTool,
28
+ signal = null,
29
+ runId = null,
30
+ attempt = null,
31
+ timeoutMs = null,
32
+ pollBusy = new Set(),
33
+ pollIntervalMs = 250,
34
+ } = {}) {
35
+ if (!session) throw new Error('dispatcher.execute requires session.');
36
+ if (!assignment?.serverName) throw new Error(`No MCP server found for agent ${assignment?.agentInstanceId ?? '(unknown)'}.`);
37
+ const serverName = assignment.serverName;
38
+ const executeTool = toolNameFor(session, serverName, 'agent_execute');
39
+ const statusTool = toolNameFor(session, serverName, 'agent_status');
40
+ const cancelTool = toolNameFor(session, serverName, 'agent_cancel');
41
+ const taskTimeoutMs = resolvedTimeoutMs(assignment, timeoutMs);
42
+ const deadline = Date.now() + taskTimeoutMs;
43
+ let jobId = null;
44
+ let lastStatus = null;
45
+
46
+ try {
47
+ emitRuntimeLog(session, taskLogPayload('agent_execute', task, assignment, {
48
+ runId,
49
+ attempt,
50
+ detail: executeTool,
51
+ }));
52
+ const accepted = parseToolPayload(await callTool(
53
+ session.mcp,
54
+ serverName,
55
+ executeTool,
56
+ executeRequest(task, session),
57
+ signal,
58
+ ));
59
+ if (accepted?.accepted === false || accepted?.ok === false) {
60
+ throw new Error(String(accepted.error ?? 'agent_execute rejected task'));
61
+ }
62
+ jobId = String(accepted.jobId ?? '');
63
+ if (!jobId) throw new Error('agent_execute did not return jobId.');
64
+ emitRuntimeLog(session, taskLogPayload('job.accepted', task, assignment, {
65
+ runId,
66
+ attempt,
67
+ jobId,
68
+ agentId: accepted.agentId ?? null,
69
+ detail: accepted.status ?? 'accepted',
70
+ }));
71
+ dispatchAgentEvent(session, createAgentEvent('task.started', {
72
+ origin: 'dispatcher',
73
+ runId,
74
+ taskId: String(task.id ?? task.step),
75
+ payload: {
76
+ runId,
77
+ taskId: String(task.id ?? task.step),
78
+ attemptId: attempt?.attemptId ?? null,
79
+ agentInstanceId: assignment.agentInstanceId,
80
+ jobId,
81
+ startedAt: new Date().toISOString(),
82
+ },
83
+ }));
84
+ dispatchTaskActivity(session, task, assignment, jobId, statusTool, runId);
85
+
86
+ while (true) {
87
+ throwIfAborted(signal);
88
+ if (Date.now() > deadline) {
89
+ throw new Error(`Task timed out after ${taskTimeoutMs}ms.`);
90
+ }
91
+ await pollActivitiesOnce(session, {
92
+ pollBusy,
93
+ signal,
94
+ callTool: async (mcp, pollServer, pollTool, args, pollSignal) => {
95
+ const result = await callTool(mcp, pollServer, pollTool, args, pollSignal);
96
+ if (pollServer === serverName && pollTool === statusTool && String(args?.jobId ?? '') === jobId) {
97
+ lastStatus = parseToolPayload(result);
98
+ }
99
+ return result;
100
+ },
101
+ });
102
+ if (!lastStatus) {
103
+ emitRuntimeLog(session, taskLogPayload('agent_status', task, assignment, {
104
+ runId,
105
+ attempt,
106
+ jobId,
107
+ detail: statusTool,
108
+ }));
109
+ lastStatus = parseToolPayload(await callTool(session.mcp, serverName, statusTool, { jobId }, signal));
110
+ }
111
+ if (isTerminal(lastStatus?.status ?? lastStatus?.result?.status)) {
112
+ emitRuntimeLog(session, taskLogPayload('task.result_returned', task, assignment, {
113
+ runId,
114
+ attempt,
115
+ jobId,
116
+ status: lastStatus?.result?.status ?? lastStatus?.status,
117
+ outputs: lastStatus?.result?.outputRefs ?? [],
118
+ error: lastStatus?.result?.error?.code ?? lastStatus?.result?.error?.message ?? null,
119
+ detail: 'terminal status',
120
+ }));
121
+ return taskResultFromStatus(task, assignment, jobId, lastStatus, attempt);
122
+ }
123
+ await delay(pollIntervalMs, signal);
124
+ emitRuntimeLog(session, taskLogPayload('agent_status', task, assignment, {
125
+ runId,
126
+ attempt,
127
+ jobId,
128
+ detail: statusTool,
129
+ }));
130
+ lastStatus = parseToolPayload(await callTool(session.mcp, serverName, statusTool, { jobId }, signal));
131
+ if (isTerminal(lastStatus?.status ?? lastStatus?.result?.status)) {
132
+ emitRuntimeLog(session, taskLogPayload('task.result_returned', task, assignment, {
133
+ runId,
134
+ attempt,
135
+ jobId,
136
+ status: lastStatus?.result?.status ?? lastStatus?.status,
137
+ outputs: lastStatus?.result?.outputRefs ?? [],
138
+ error: lastStatus?.result?.error?.code ?? lastStatus?.result?.error?.message ?? null,
139
+ detail: 'terminal status',
140
+ }));
141
+ return taskResultFromStatus(task, assignment, jobId, lastStatus, attempt);
142
+ }
143
+ }
144
+ } catch (error) {
145
+ if (isAbortError(error) && jobId) {
146
+ await callTool(session.mcp, serverName, cancelTool, { jobId }, null).catch(() => null);
147
+ }
148
+ throw error;
149
+ } finally {
150
+ emitRuntimeLog(session, taskLogPayload('lock.released', task, assignment, {
151
+ runId,
152
+ attempt,
153
+ jobId,
154
+ detail: attempt?.locks?.join(',') || 'no locks',
155
+ }));
156
+ attempt?.release?.();
157
+ }
158
+ }
159
+
160
+ function executeRequest(task, session) {
161
+ return {
162
+ taskId: String(task.id ?? task.step),
163
+ idempotencyKey: task.idempotencyKey ?? undefined,
164
+ operation: task.operation,
165
+ workspace: workspaceRequest(session),
166
+ arguments: task.arguments && typeof task.arguments === 'object' ? task.arguments : {},
167
+ constraints: {
168
+ requireApprovalForMutations: task.requiresApproval === true,
169
+ },
170
+ };
171
+ }
172
+
173
+ function workspaceRequest(session) {
174
+ const workspace = session.workspace ?? session._currentRunIdentity?.workspace;
175
+ if (workspace && typeof workspace === 'object' && !Array.isArray(workspace)) return { ...workspace };
176
+ return { name: String(workspace ?? 'workspace') };
177
+ }
178
+
179
+ function dispatchTaskActivity(session, task, assignment, jobId, statusTool, runId) {
180
+ const activity = normalizeActivity({
181
+ id: jobId,
182
+ source: assignment.serverName,
183
+ kind: task.operation ?? task.requiredCapability ?? 'task',
184
+ label: task.label ?? task.description ?? String(task.id ?? task.step),
185
+ status: 'queued',
186
+ progress: { percent: 0, stepId: String(task.id ?? task.step) },
187
+ poll: {
188
+ server: assignment.serverName,
189
+ tool: statusTool,
190
+ args: { jobId },
191
+ intervalMs: 1000,
192
+ },
193
+ outputRefs: [],
194
+ });
195
+ dispatchAgentEvent(session, createAgentEvent('activity_upserted', {
196
+ origin: 'dispatcher',
197
+ runId,
198
+ taskId: String(task.id ?? task.step),
199
+ payload: { activity },
200
+ }));
201
+ }
202
+
203
+ function taskResultFromStatus(task, assignment, jobId, statusPayload, attempt = null) {
204
+ const result = statusPayload?.result ?? {};
205
+ const resultStatus = String(result.status ?? statusPayload?.status ?? '').toLowerCase();
206
+ const ok = ['succeeded', 'success', 'done', 'complete', 'completed'].includes(resultStatus);
207
+ return {
208
+ ok,
209
+ taskId: String(task.id ?? task.step),
210
+ attemptId: statusPayload?.attemptId ?? attempt?.attemptId ?? null,
211
+ jobId,
212
+ agentInstanceId: assignment.agentInstanceId,
213
+ status: result.status ?? statusPayload?.status,
214
+ outputRefs: Array.isArray(result.outputRefs) ? result.outputRefs : [],
215
+ metrics: result.metrics ?? {},
216
+ error: result.error ?? null,
217
+ rawStatus: statusPayload,
218
+ };
219
+ }
220
+
221
+ function toolNameFor(session, serverName, baseName) {
222
+ const tools = session.mcp?.[serverName]?.tools ?? [];
223
+ const names = tools.map((tool) => String(tool.name ?? '')).filter(Boolean);
224
+ return names.find((name) => name === baseName)
225
+ ?? names.find((name) => name === `${serverName}__${baseName}`)
226
+ ?? names.find((name) => name.endsWith(`__${baseName}`))
227
+ ?? baseName;
228
+ }
229
+
230
+ function parseToolPayload(result) {
231
+ if (result && typeof result === 'object' && !Array.isArray(result) && !Array.isArray(result.content)) return result;
232
+ return parseJsonText(formatMcpToolResult(result)) ?? {};
233
+ }
234
+
235
+ function resolvedTimeoutMs(assignment, timeoutMs) {
236
+ const agentLimit = Number(assignment?.agent?.description?.limits?.maxTaskDurationMs ?? assignment?.description?.limits?.maxTaskDurationMs);
237
+ if (Number.isFinite(agentLimit) && agentLimit > 0) return agentLimit;
238
+ const runtimeLimit = Number(timeoutMs);
239
+ return Number.isFinite(runtimeLimit) && runtimeLimit > 0 ? runtimeLimit : 600_000;
240
+ }
241
+
242
+ function taskLogPayload(event, task, assignment, {
243
+ runId = null,
244
+ attempt = null,
245
+ jobId = null,
246
+ agentId = null,
247
+ status = null,
248
+ outputs = null,
249
+ error = null,
250
+ detail = null,
251
+ } = {}) {
252
+ return {
253
+ event,
254
+ runId,
255
+ planRevision: task?.planRevision ?? null,
256
+ groupId: task?.groupId ?? null,
257
+ taskId: String(task?.id ?? task?.step),
258
+ attemptId: attempt?.attemptId ?? null,
259
+ agentType: assignment?.agent?.description?.agentType ?? assignment?.description?.agentType ?? null,
260
+ agentInstanceId: assignment?.agentInstanceId ?? null,
261
+ agentId: agentId ?? assignment?.agentId ?? null,
262
+ jobId,
263
+ capability: task?.requiredCapability ?? assignment?.capability ?? null,
264
+ operation: task?.operation ?? assignment?.operation ?? null,
265
+ status,
266
+ outputs,
267
+ error,
268
+ detail,
269
+ };
270
+ }
271
+
272
+ function isTerminal(status) {
273
+ return TERMINAL_STATUSES.has(String(status ?? '').toLowerCase());
274
+ }
275
+
276
+ function delay(ms, signal) {
277
+ return new Promise((resolve, reject) => {
278
+ if (signal?.aborted) {
279
+ reject(abortError());
280
+ return;
281
+ }
282
+ const timer = setTimeout(resolve, Math.max(0, Number(ms) || 0));
283
+ signal?.addEventListener('abort', () => {
284
+ clearTimeout(timer);
285
+ reject(abortError());
286
+ }, { once: true });
287
+ });
288
+ }
289
+
290
+ function throwIfAborted(signal) {
291
+ if (signal?.aborted) throw abortError();
292
+ }
293
+
294
+ function abortError() {
295
+ const error = new Error('Runtime run cancelled.');
296
+ error.name = 'AbortError';
297
+ return error;
298
+ }
299
+
300
+ function isAbortError(error) {
301
+ return error?.name === 'AbortError';
302
+ }