@dotdrelle/wiki-manager 0.15.66 → 0.15.71
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/.env.example +10 -3
- package/README.md +57 -0
- package/agent-runtimes.example.json +68 -0
- package/agents.docker-compose.yml +39 -1
- package/docker-compose.yml +3 -3
- package/package.json +3 -2
- package/src/activity/activityAggregator.test.js +2 -2
- package/src/agent/graph.js +13 -11
- package/src/agent/skillRecursion.test.js +13 -12
- package/src/cli/wiki-manager.js +125 -37
- package/src/cli/wiki-manager.test.js +16 -16
- package/src/commands/slash.js +59 -5
- package/src/contracts/schemas.js +67 -0
- package/src/core/activity.js +5 -0
- package/src/core/agentEvents.js +139 -25
- package/src/core/agentEvents.test.js +26 -1
- package/src/core/buildInfo.json +2 -2
- package/src/core/commandFailure.test.js +2 -2
- package/src/core/currentArtifact.test.js +5 -5
- package/src/core/dockerCompose.test.js +8 -40
- package/src/core/env.js +14 -0
- package/src/core/env.test.js +19 -0
- package/src/core/googleGrants.test.js +1 -1
- package/src/core/mcp.js +1 -1
- package/src/core/mcp.test.js +1 -1
- package/src/core/otherWorkspacesRunning.test.js +6 -6
- package/src/core/runtimeEventAdapter.js +81 -0
- package/src/core/runtimeEventAdapter.test.js +61 -0
- package/src/core/runtimeLog.js +35 -1
- package/src/core/runtimeLog.test.js +27 -2
- package/src/core/skillChainView.test.js +2 -2
- package/src/core/skillCompiler.test.js +1 -1
- package/src/core/skillInvocation.js +13 -8
- package/src/core/skillInvocation.test.js +1 -1
- package/src/core/startupCheck.js +58 -0
- package/src/core/startupCheck.test.js +29 -1
- package/src/core/wikiSetup.js +25 -0
- package/src/core/wikiSetup.test.js +35 -0
- package/src/core/wikirc.test.js +6 -6
- package/src/core/workspaceInherit.test.js +14 -14
- package/src/orchestrator/agentRegistry.js +1 -22
- package/src/orchestrator/agentRegistry.test.js +6 -6
- package/src/orchestrator/assignmentManager.js +16 -4
- package/src/orchestrator/capabilityRegistry.js +8 -1
- package/src/orchestrator/dispatcher.js +405 -2
- package/src/orchestrator/dispatcher.test.js +158 -4
- package/src/orchestrator/objectiveResolver.js +10 -6
- package/src/orchestrator/objectiveResolver.test.js +26 -27
- package/src/orchestrator/providers/deepAgentsProvider.js +168 -0
- package/src/orchestrator/providers/deepAgentsProvider.test.js +178 -0
- package/src/orchestrator/providers/dispatcherExternalRuntime.test.js +409 -0
- package/src/orchestrator/providers/fakeRuntimeProvider.js +164 -0
- package/src/orchestrator/providers/fakeRuntimeProvider.test.js +201 -0
- package/src/orchestrator/providers/runtimeProvider.js +101 -0
- package/src/orchestrator/providers/runtimeProviders.js +378 -0
- package/src/orchestrator/providers/runtimeProviders.test.js +384 -0
- package/src/orchestrator/resultAggregator.js +35 -2
- package/src/orchestrator/resultAggregator.test.js +62 -0
- package/src/orchestrator/scheduler.test.js +4 -4
- package/src/runtime/delegation.test.js +11 -11
- package/src/runtime/recoveryManager.js +70 -5
- package/src/runtime/runner.test.js +1 -1
- package/src/runtime/server.test.js +2 -2
- package/src/runtime/skillChain.e2e.test.js +2 -2
- package/src/runtime/store.test.js +8 -5
- package/src/runtime/supervisor.js +5 -10
- package/src/runtime/workspaceIsolation.test.js +26 -26
- package/src/shell/RightPane.tsx +23 -3
- package/src/shell/StartupScreen.tsx +44 -7
- package/src/shell/repl.js +24 -2
- package/src/shell/repl.test.js +13 -0
- package/wiki-workspace +53 -3
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { capabilityRegistryForSession } from './capabilityRegistry.js';
|
|
2
2
|
import { CapabilityUnavailableError, resolve } from './capabilityResolver.js';
|
|
3
3
|
|
|
4
4
|
export function createAssignmentManager({
|
|
@@ -27,9 +27,7 @@ export async function assign(task, {
|
|
|
27
27
|
if (!capability) {
|
|
28
28
|
throw new CapabilityUnavailableError(capability, 'task_missing_required_capability', { taskId: task?.id ?? task?.step });
|
|
29
29
|
}
|
|
30
|
-
const effectiveRegistry = registry ?? session
|
|
31
|
-
agents: session?.agentRegistrySnapshot ?? session?.agents ?? [],
|
|
32
|
-
});
|
|
30
|
+
const effectiveRegistry = registry ?? capabilityRegistryForSession(session);
|
|
33
31
|
const effectiveWorkspaceConfig = workspaceConfig ?? session?.wikircConfig ?? session?.wikirc?.config ?? {};
|
|
34
32
|
const retryAssignment = task?.retryAssignment;
|
|
35
33
|
if (retryAssignment?.agentInstanceId) {
|
|
@@ -46,6 +44,14 @@ export async function assign(task, {
|
|
|
46
44
|
capability,
|
|
47
45
|
operation: task?.operation ?? null,
|
|
48
46
|
serverName: agent?.serverName ?? provider?.serverName ?? null,
|
|
47
|
+
// Mirrors the primary resolution branch below: without this, retrying a
|
|
48
|
+
// task assigned to an external-runtime provider drops the routing
|
|
49
|
+
// discriminator and the dispatcher misroutes it to the MCP path, which
|
|
50
|
+
// throws "No MCP server found" since external-runtime agents carry no
|
|
51
|
+
// serverName.
|
|
52
|
+
providerKind: provider?.providerKind ?? 'mcp-agent',
|
|
53
|
+
runtimeProvider: provider?.runtimeProvider ?? null,
|
|
54
|
+
runtimeId: provider?.runtimeId ?? null,
|
|
49
55
|
agent,
|
|
50
56
|
retry: true,
|
|
51
57
|
previousAgentInstanceId: retryAssignment.previousAgentInstanceId ?? null,
|
|
@@ -62,6 +68,12 @@ export async function assign(task, {
|
|
|
62
68
|
capability,
|
|
63
69
|
operation: task?.operation ?? null,
|
|
64
70
|
serverName: agent?.serverName ?? provider?.serverName ?? null,
|
|
71
|
+
// External runtime providers (RFC § 8) resolve through the same registry
|
|
72
|
+
// as MCP agents; the assignment carries the routing discriminator so the
|
|
73
|
+
// dispatcher can hand the task to the runtime instead of agent_execute.
|
|
74
|
+
providerKind: provider?.providerKind ?? 'mcp-agent',
|
|
75
|
+
runtimeProvider: provider?.runtimeProvider ?? null,
|
|
76
|
+
runtimeId: provider?.runtimeId ?? null,
|
|
65
77
|
agent,
|
|
66
78
|
};
|
|
67
79
|
}
|
|
@@ -19,6 +19,11 @@ export function createCapabilityRegistry({ agents = [], compatibleContractVersio
|
|
|
19
19
|
capability,
|
|
20
20
|
description: agent.description,
|
|
21
21
|
lastSeenAt: agent.lastSeenAt ?? null,
|
|
22
|
+
// External runtime providers (RFC § 8) ride the same registry as MCP
|
|
23
|
+
// agents. These fields are null for every ordinary MCP agent.
|
|
24
|
+
providerKind: agent.providerKind ?? null,
|
|
25
|
+
runtimeId: agent.runtimeId ?? null,
|
|
26
|
+
runtimeProvider: agent.runtimeProvider ?? null,
|
|
22
27
|
};
|
|
23
28
|
const list = providers.get(key) ?? [];
|
|
24
29
|
list.push(entry);
|
|
@@ -54,7 +59,9 @@ export function capabilityRegistryForSession(session) {
|
|
|
54
59
|
?? session?.agentRegistrySnapshot
|
|
55
60
|
?? session?.agents
|
|
56
61
|
?? [];
|
|
57
|
-
|
|
62
|
+
const runtimeAgents = session?.runtimeProviderAgents ?? [];
|
|
63
|
+
const merged = [...agents, ...runtimeAgents];
|
|
64
|
+
if (merged.length > 0) return createCapabilityRegistry({ agents: merged });
|
|
58
65
|
if (session?.capabilityRegistry?.providersFor) return session.capabilityRegistry;
|
|
59
66
|
return createCapabilityRegistry();
|
|
60
67
|
}
|
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
import { normalizeActivity, parseJsonText } from '../core/activity.js';
|
|
2
2
|
import { createAgentEvent, dispatchAgentEvent } from '../core/agentEvents.js';
|
|
3
3
|
import { callMcpTool, formatMcpToolResult } from '../core/mcp.js';
|
|
4
|
+
import { loadWorkspaceProfile } from '../core/profile.js';
|
|
5
|
+
import { mapRuntimeEvent } from '../core/runtimeEventAdapter.js';
|
|
4
6
|
import { emitRuntimeLog, pollActivitiesOnce } from '../runtime/supervisor.js';
|
|
7
|
+
import { APPROVAL_DEFAULT_CLASS, approvalCovered } from './approvalPolicy.js';
|
|
5
8
|
import { isSuccessful, isTerminal } from './taskStatuses.js';
|
|
6
9
|
|
|
7
10
|
|
|
@@ -32,6 +35,16 @@ export async function execute(task, assignment, {
|
|
|
32
35
|
pollBusy = new Set(),
|
|
33
36
|
pollIntervalMs = 2500,
|
|
34
37
|
} = {}) {
|
|
38
|
+
if (isExternalRuntimeAssignment(assignment)) {
|
|
39
|
+
return executeExternalRuntime(task, assignment, {
|
|
40
|
+
session,
|
|
41
|
+
signal,
|
|
42
|
+
runId,
|
|
43
|
+
attempt,
|
|
44
|
+
timeoutMs,
|
|
45
|
+
pollIntervalMs,
|
|
46
|
+
});
|
|
47
|
+
}
|
|
35
48
|
if (!session) throw new Error('dispatcher.execute requires session.');
|
|
36
49
|
if (!assignment?.serverName) throw new Error(`No MCP server found for agent ${assignment?.agentInstanceId ?? '(unknown)'}.`);
|
|
37
50
|
const serverName = assignment.serverName;
|
|
@@ -157,6 +170,243 @@ export async function execute(task, assignment, {
|
|
|
157
170
|
}
|
|
158
171
|
}
|
|
159
172
|
|
|
173
|
+
function isExternalRuntimeAssignment(assignment) {
|
|
174
|
+
return assignment?.providerKind === 'external-runtime'
|
|
175
|
+
&& typeof assignment?.runtimeProvider?.execute === 'function';
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// A proposal without mutations still waits for a human grant: the request is
|
|
179
|
+
// treated as the default approval class, covered by a run-scope grant like
|
|
180
|
+
// any other. Every announced class must be covered — the "global" approval is
|
|
181
|
+
// bounded by the proposal.
|
|
182
|
+
function pendingApprovalCovered(classes, approvals, context) {
|
|
183
|
+
const list = Array.isArray(classes) && classes.length > 0
|
|
184
|
+
? classes
|
|
185
|
+
: [APPROVAL_DEFAULT_CLASS];
|
|
186
|
+
return list.every((approvalClass) => approvalCovered(
|
|
187
|
+
{
|
|
188
|
+
// Without an id, grantCoversTask's task/tool-scope match (which keys
|
|
189
|
+
// off task.id/localId/taskId) can never succeed, so a per-task
|
|
190
|
+
// `/approve item <id>` grant silently fails to cover this task and only
|
|
191
|
+
// a run-scope "approve all" can ever unblock it.
|
|
192
|
+
id: context?.taskId ?? null,
|
|
193
|
+
taskId: context?.taskId ?? null,
|
|
194
|
+
groupId: context?.groupId ?? null,
|
|
195
|
+
requiresApproval: true,
|
|
196
|
+
approvalClass: String(approvalClass),
|
|
197
|
+
},
|
|
198
|
+
approvals,
|
|
199
|
+
context,
|
|
200
|
+
));
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
async function executeExternalRuntime(task, assignment, {
|
|
204
|
+
session,
|
|
205
|
+
signal = null,
|
|
206
|
+
runId = null,
|
|
207
|
+
attempt = null,
|
|
208
|
+
timeoutMs = null,
|
|
209
|
+
pollIntervalMs = 2500,
|
|
210
|
+
} = {}) {
|
|
211
|
+
if (!session) throw new Error('dispatcher.execute requires session.');
|
|
212
|
+
const runtimeProvider = assignment.runtimeProvider;
|
|
213
|
+
const taskId = String(task.id ?? task.step);
|
|
214
|
+
const taskTimeoutMs = resolvedTimeoutMs(assignment, timeoutMs);
|
|
215
|
+
let deadline = Date.now() + taskTimeoutMs;
|
|
216
|
+
let runtimeRunId = null;
|
|
217
|
+
let unsubscribe = null;
|
|
218
|
+
let pendingApproval = null;
|
|
219
|
+
try {
|
|
220
|
+
emitRuntimeLog(session, taskLogPayload('runtime.execute', task, assignment, {
|
|
221
|
+
runId,
|
|
222
|
+
attempt,
|
|
223
|
+
detail: 'external-runtime',
|
|
224
|
+
}));
|
|
225
|
+
const mcpPool = activeProfileMcp(session);
|
|
226
|
+
if (!mcpPool) {
|
|
227
|
+
// A degradation must announce itself. The runtime's whole value is its
|
|
228
|
+
// eyes: dispatched without the workspace wiki MCP, the Deep Agent
|
|
229
|
+
// improvises with its built-in backend (it ran `ls /` on the gateway
|
|
230
|
+
// container and answered "the workspace is empty" — a plausible,
|
|
231
|
+
// confident, entirely wrong answer). Say it in the journal, keep the
|
|
232
|
+
// run (the objective may still be answerable from the model alone),
|
|
233
|
+
// but never let blindness pass silently.
|
|
234
|
+
emitRuntimeLog(session, taskLogPayload('runtime.blind', task, assignment, {
|
|
235
|
+
runId,
|
|
236
|
+
attempt,
|
|
237
|
+
detail: 'no workspace wiki MCP pool — the endpoint is down or declares no read tools; the runtime will run without its eyes',
|
|
238
|
+
}));
|
|
239
|
+
}
|
|
240
|
+
const accepted = await runtimeProvider.execute({
|
|
241
|
+
objective: task.label ?? task.description ?? taskId,
|
|
242
|
+
operation: task.operation ?? null,
|
|
243
|
+
capability: task.requiredCapability ?? null,
|
|
244
|
+
arguments: task.arguments && typeof task.arguments === 'object' ? task.arguments : {},
|
|
245
|
+
workspace: workspaceRequest(session),
|
|
246
|
+
model: activeProfileModel(session),
|
|
247
|
+
language: session?.language ?? session?.wikircConfig?.language ?? null,
|
|
248
|
+
mcp: mcpPool,
|
|
249
|
+
systemPrompt: activeRuntimeSystemPrompt(session, task, assignment),
|
|
250
|
+
});
|
|
251
|
+
runtimeRunId = String(accepted?.runId ?? '');
|
|
252
|
+
if (!runtimeRunId) throw new Error('runtime.execute did not return runId.');
|
|
253
|
+
emitRuntimeLog(session, taskLogPayload('runtime.accepted', task, assignment, {
|
|
254
|
+
runId,
|
|
255
|
+
attempt,
|
|
256
|
+
jobId: runtimeRunId,
|
|
257
|
+
detail: String(accepted?.status ?? 'running'),
|
|
258
|
+
}));
|
|
259
|
+
dispatchAgentEvent(session, createAgentEvent('task.started', {
|
|
260
|
+
origin: 'dispatcher',
|
|
261
|
+
runId,
|
|
262
|
+
taskId,
|
|
263
|
+
payload: {
|
|
264
|
+
runId,
|
|
265
|
+
taskId,
|
|
266
|
+
attemptId: attempt?.attemptId ?? null,
|
|
267
|
+
agentInstanceId: assignment.agentInstanceId,
|
|
268
|
+
jobId: runtimeRunId,
|
|
269
|
+
startedAt: new Date().toISOString(),
|
|
270
|
+
},
|
|
271
|
+
}));
|
|
272
|
+
dispatchExternalRuntimeActivity(session, task, assignment, runtimeRunId, 'running', runId);
|
|
273
|
+
if (typeof runtimeProvider.subscribe === 'function') {
|
|
274
|
+
unsubscribe = runtimeProvider.subscribe(runtimeRunId, (event) => {
|
|
275
|
+
for (const mapped of mapRuntimeEvent(event)) {
|
|
276
|
+
dispatchAgentEvent(session, createAgentEvent(mapped.type, {
|
|
277
|
+
origin: 'runtime_provider',
|
|
278
|
+
runId,
|
|
279
|
+
taskId,
|
|
280
|
+
payload: mapped.payload,
|
|
281
|
+
}));
|
|
282
|
+
if (mapped.type === 'approval.requested') {
|
|
283
|
+
pendingApproval = {
|
|
284
|
+
approvalId: mapped.payload?.approvalId ?? null,
|
|
285
|
+
approvalClasses: Array.isArray(mapped.payload?.approvalClasses)
|
|
286
|
+
? mapped.payload.approvalClasses
|
|
287
|
+
: [],
|
|
288
|
+
};
|
|
289
|
+
const summary = String(mapped.payload?.proposal?.summary ?? mapped.payload?.reason ?? '').trim();
|
|
290
|
+
dispatchAgentEvent(session, createAgentEvent('assistant_message', {
|
|
291
|
+
origin: 'runtime_provider',
|
|
292
|
+
runId,
|
|
293
|
+
payload: {
|
|
294
|
+
content: [
|
|
295
|
+
'⏸ Approval required before execution:',
|
|
296
|
+
...(summary ? [` ${summary}`] : []),
|
|
297
|
+
'Type /approve (or click "Approve") to proceed, "cancel" to abandon.',
|
|
298
|
+
].filter(Boolean).join('\n'),
|
|
299
|
+
},
|
|
300
|
+
}));
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
});
|
|
304
|
+
}
|
|
305
|
+
let approvalWaitStartedAt = null;
|
|
306
|
+
while (true) {
|
|
307
|
+
throwIfAborted(signal);
|
|
308
|
+
// While a proposal waits for a human decision, the task timeout does
|
|
309
|
+
// not tick — the human decides, not the clock (the scheduler applies
|
|
310
|
+
// the same rule to its approval waits). The elapsed wait is credited
|
|
311
|
+
// back onto the deadline once the gate clears (below), rather than
|
|
312
|
+
// merely skipped from this check, so a late approval does not
|
|
313
|
+
// immediately expire the task on the very next iteration.
|
|
314
|
+
if (!pendingApproval && Date.now() > deadline) {
|
|
315
|
+
await runtimeProvider.cancel(runtimeRunId).catch(() => null);
|
|
316
|
+
throw new Error(`Task timed out after ${taskTimeoutMs}ms.`);
|
|
317
|
+
}
|
|
318
|
+
if (pendingApproval) {
|
|
319
|
+
approvalWaitStartedAt ??= Date.now();
|
|
320
|
+
const approvalBeingChecked = pendingApproval;
|
|
321
|
+
const approvals = session.agentProjection?.approvals ?? session.approvals ?? [];
|
|
322
|
+
const covered = pendingApprovalCovered(approvalBeingChecked.approvalClasses, approvals, {
|
|
323
|
+
runId,
|
|
324
|
+
taskId,
|
|
325
|
+
groupId: task.groupId ?? null,
|
|
326
|
+
workspaceId: session.workspace ?? null,
|
|
327
|
+
planRevision: session.planRevision ?? session.agentProjection?.planRevision ?? null,
|
|
328
|
+
});
|
|
329
|
+
if (covered && typeof runtimeProvider.approve === 'function') {
|
|
330
|
+
emitRuntimeLog(session, taskLogPayload('runtime.approval_granted', task, assignment, {
|
|
331
|
+
runId,
|
|
332
|
+
attempt,
|
|
333
|
+
jobId: runtimeRunId,
|
|
334
|
+
detail: 'unblocking runtime HITL',
|
|
335
|
+
}));
|
|
336
|
+
await runtimeProvider.approve(runtimeRunId, { approved: true, scope: approvalBeingChecked.approvalClasses });
|
|
337
|
+
// Only clear the approval just resolved: the runtime's event stream
|
|
338
|
+
// may have raised a second, unrelated one while the approve() round
|
|
339
|
+
// trip above was in flight.
|
|
340
|
+
if (pendingApproval === approvalBeingChecked) {
|
|
341
|
+
pendingApproval = null;
|
|
342
|
+
deadline += Date.now() - approvalWaitStartedAt;
|
|
343
|
+
approvalWaitStartedAt = null;
|
|
344
|
+
}
|
|
345
|
+
} else {
|
|
346
|
+
await delay(pollIntervalMs, signal);
|
|
347
|
+
continue;
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
const lastStatus = await runtimeProvider.status(runtimeRunId);
|
|
351
|
+
if (isTerminal(lastStatus?.status)) {
|
|
352
|
+
const refusedParams = Array.isArray(lastStatus?.result?.refusedParams)
|
|
353
|
+
? lastStatus.result.refusedParams
|
|
354
|
+
: [];
|
|
355
|
+
if (refusedParams.length > 0) {
|
|
356
|
+
emitRuntimeLog(session, taskLogPayload('runtime.params_refused', task, assignment, {
|
|
357
|
+
runId,
|
|
358
|
+
attempt,
|
|
359
|
+
jobId: runtimeRunId,
|
|
360
|
+
detail: `model refused sampling parameters (${refusedParams.join(', ')}) — remove them from the workspace .wikirc (llm.<key>) so they are no longer sent`,
|
|
361
|
+
}));
|
|
362
|
+
}
|
|
363
|
+
emitRuntimeLog(session, taskLogPayload('runtime.result_returned', task, assignment, {
|
|
364
|
+
runId,
|
|
365
|
+
attempt,
|
|
366
|
+
jobId: runtimeRunId,
|
|
367
|
+
status: lastStatus.status,
|
|
368
|
+
detail: 'terminal status',
|
|
369
|
+
}));
|
|
370
|
+
dispatchExternalRuntimeActivity(session, task, assignment, runtimeRunId, lastStatus.status, runId);
|
|
371
|
+
return taskResultFromStatus(task, assignment, runtimeRunId, lastStatus, attempt);
|
|
372
|
+
}
|
|
373
|
+
await delay(pollIntervalMs, signal);
|
|
374
|
+
}
|
|
375
|
+
} catch (error) {
|
|
376
|
+
if (isAbortError(error) && runtimeRunId) {
|
|
377
|
+
await runtimeProvider.cancel(runtimeRunId).catch(() => null);
|
|
378
|
+
}
|
|
379
|
+
throw error;
|
|
380
|
+
} finally {
|
|
381
|
+
unsubscribe?.();
|
|
382
|
+
emitRuntimeLog(session, taskLogPayload('lock.released', task, assignment, {
|
|
383
|
+
runId,
|
|
384
|
+
attempt,
|
|
385
|
+
jobId: runtimeRunId,
|
|
386
|
+
detail: attempt?.locks?.join(',') || 'no locks',
|
|
387
|
+
}));
|
|
388
|
+
attempt?.release?.();
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
function dispatchExternalRuntimeActivity(session, task, assignment, runtimeRunId, status, runId) {
|
|
393
|
+
const activity = normalizeActivity({
|
|
394
|
+
id: runtimeRunId,
|
|
395
|
+
source: assignment.runtimeId ?? 'external-runtime',
|
|
396
|
+
kind: task.operation ?? task.requiredCapability ?? 'task',
|
|
397
|
+
label: task.label ?? task.description ?? String(task.id ?? task.step),
|
|
398
|
+
status,
|
|
399
|
+
progress: { percent: isTerminal(status) ? 100 : 0, stepId: String(task.id ?? task.step) },
|
|
400
|
+
outputRefs: [],
|
|
401
|
+
});
|
|
402
|
+
dispatchAgentEvent(session, createAgentEvent('activity_upserted', {
|
|
403
|
+
origin: 'dispatcher',
|
|
404
|
+
runId,
|
|
405
|
+
taskId: String(task.id ?? task.step),
|
|
406
|
+
payload: { activity },
|
|
407
|
+
}));
|
|
408
|
+
}
|
|
409
|
+
|
|
160
410
|
function executeRequest(task, session, runId) {
|
|
161
411
|
return {
|
|
162
412
|
taskId: String(task.id ?? task.step),
|
|
@@ -165,7 +415,20 @@ function executeRequest(task, session, runId) {
|
|
|
165
415
|
idempotencyKey: task.idempotencyKey ?? undefined,
|
|
166
416
|
operation: task.operation,
|
|
167
417
|
workspace: workspaceRequest(session),
|
|
168
|
-
arguments:
|
|
418
|
+
arguments: {
|
|
419
|
+
...(task.arguments && typeof task.arguments === 'object' ? task.arguments : {}),
|
|
420
|
+
// The production agent's confirmation guard
|
|
421
|
+
// (PRODUCTION_REQUIRE_CONFIRMATION=true) has one contract: "only an
|
|
422
|
+
// approved task may run a mutating operation — Donna passes
|
|
423
|
+
// confirm=true after the run-scope approval". The scheduler never
|
|
424
|
+
// dispatches a requiresApproval task before coverage
|
|
425
|
+
// (dependencyResolver.readyTasks), so reaching dispatch IS the
|
|
426
|
+
// approval. Without this, an operator who enables the guard sees every
|
|
427
|
+
// mutating production job fail with "requires confirm=true" — the
|
|
428
|
+
// first E2E ingest plan dispatched by the deep agent's
|
|
429
|
+
// planExpansionRequest failed exactly that way, 19 tasks in one batch.
|
|
430
|
+
...(task.requiresApproval === true ? { confirm: true } : {}),
|
|
431
|
+
},
|
|
169
432
|
constraints: {
|
|
170
433
|
requireApprovalForMutations: task.requiresApproval === true,
|
|
171
434
|
},
|
|
@@ -178,6 +441,137 @@ function workspaceRequest(session) {
|
|
|
178
441
|
return { name: String(workspace ?? 'workspace') };
|
|
179
442
|
}
|
|
180
443
|
|
|
444
|
+
// The model travels WITH the run: the runtime is workspace-agnostic and must
|
|
445
|
+
// follow the active profile — default or `/config use` — without a config
|
|
446
|
+
// sync. Numeric LLM parameters declared by the profile (temperature, …) ride
|
|
447
|
+
// along too: nothing is hardcoded here, the workspace config is the source.
|
|
448
|
+
function activeProfileModel(session) {
|
|
449
|
+
const llm = session?.wikircConfig?.llm ?? {};
|
|
450
|
+
const model = {
|
|
451
|
+
...(llm.baseUrl ? { baseUrl: String(llm.baseUrl) } : {}),
|
|
452
|
+
...(llm.model ? { model: String(llm.model) } : {}),
|
|
453
|
+
...(llm.apiKey ? { apiKey: String(llm.apiKey) } : {}),
|
|
454
|
+
};
|
|
455
|
+
for (const key of ['temperature', 'maxTokens', 'topP', 'seed']) {
|
|
456
|
+
const value = Number(llm[key]);
|
|
457
|
+
if (Number.isFinite(value)) model[key] = value;
|
|
458
|
+
}
|
|
459
|
+
return Object.keys(model).length > 0 ? model : null;
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
// The read-only wiki MCP tools the runtime is allowed to see. An explicit
|
|
463
|
+
// allow-list, not a denylist: the runtime has eyes (read tools) and a mouth
|
|
464
|
+
// (gated side-effects through the DAG), never hands on the workspace. A denylist
|
|
465
|
+
// keyed on "write_page|add_source|…" already silently let build_context_write
|
|
466
|
+
// and template_write through, and would leak any future wiki_delete_page /
|
|
467
|
+
// wiki_move_page / wiki_rename the moment it is added upstream.
|
|
468
|
+
const READ_ONLY_WIKI_TOOLS = new Set([
|
|
469
|
+
'help_list', 'help_read', 'help_search',
|
|
470
|
+
'profile_read', 'template_read',
|
|
471
|
+
'wiki_collect_context', 'wiki_list_ingested_sources', 'wiki_list_pages',
|
|
472
|
+
'wiki_outline', 'wiki_read_deliverable', 'wiki_read_ingested_source',
|
|
473
|
+
'wiki_read_page', 'wiki_read_pages', 'wiki_search_context',
|
|
474
|
+
'wiki_workspace_status',
|
|
475
|
+
]);
|
|
476
|
+
|
|
477
|
+
// An arbitrary external connector's tool names cannot be enumerated ahead of
|
|
478
|
+
// time the way READ_ONLY_WIKI_TOOLS can, so this stays a denylist — but a
|
|
479
|
+
// word-boundary one. The old test was a raw substring match
|
|
480
|
+
// (/write|send|create|update|add|remove/), which dropped safe reads whose name
|
|
481
|
+
// merely contained a mutating verb (`list_recent_updates`, `get_created_at`,
|
|
482
|
+
// `search_addresses`) and missed side-effecting tools that use another verb
|
|
483
|
+
// (`crawl_site`, `post_message`, `run_report`, `publish_draft`). The real
|
|
484
|
+
// guardrails against "hands on the workspace" are elsewhere —
|
|
485
|
+
// EXCLUDED_EXTERNAL_SERVERS covers every workspace-writing agent, and a
|
|
486
|
+
// per-tool `requireApproval` entry drops the whole server — this filter only
|
|
487
|
+
// keeps an obviously-mutating tool of an otherwise-safe connector out of the
|
|
488
|
+
// runtime's eyes.
|
|
489
|
+
const EXTERNAL_MUTATING_VERB = /(?:^|[_-])(?:write|send|post|create|delete|destroy|remove|drop|modify|edit|update|patch|put|upload|publish|submit|execute|run|crawl|trigger|cancel|approve|move|rename|set|add|insert|append|archive)(?:[_-]|$)/i;
|
|
490
|
+
|
|
491
|
+
function isReadOnlyExternalTool(toolName) {
|
|
492
|
+
const base = toolName.includes('__') ? toolName.slice(toolName.lastIndexOf('__') + 2) : toolName;
|
|
493
|
+
return !EXTERNAL_MUTATING_VERB.test(base);
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
// The runtime's EYES, per run: the active workspace's wiki MCP (read tools
|
|
497
|
+
// only) PLUS the declared external MCP endpoints that are safe to hand over
|
|
498
|
+
// (connected, no approval-gated tools, not a workspace-mutating server) —
|
|
499
|
+
// typically web search (exa). The allow-list here is the authority: nothing
|
|
500
|
+
// else reaches the runtime.
|
|
501
|
+
export function activeProfileMcp(session) {
|
|
502
|
+
const blocks = [];
|
|
503
|
+
const wiki = session?.mcp?.wiki;
|
|
504
|
+
if (wiki?.url && wiki.status === 'connected') {
|
|
505
|
+
const tools = (wiki.tools ?? [])
|
|
506
|
+
.map((tool) => String(tool.name ?? ''))
|
|
507
|
+
.filter((name) => {
|
|
508
|
+
if (!name) return false;
|
|
509
|
+
const base = name.includes('__') ? name.slice(name.lastIndexOf('__') + 2) : name;
|
|
510
|
+
return READ_ONLY_WIKI_TOOLS.has(base);
|
|
511
|
+
});
|
|
512
|
+
if (tools.length > 0) {
|
|
513
|
+
// Same credential contract as the manager's own MCP client (mcp.js
|
|
514
|
+
// `authorization: Bearer ${endpoint.token}`): the wiki detail carries
|
|
515
|
+
// `token`, not `headers` — without it the gateway's MCP connection is
|
|
516
|
+
// rejected by the workspace MCP server ("invalid or missing bearer token")
|
|
517
|
+
// and the Deep Agent runs blind.
|
|
518
|
+
const headers = {
|
|
519
|
+
...(wiki.headers && typeof wiki.headers === 'object' ? wiki.headers : {}),
|
|
520
|
+
...(wiki.token ? { Authorization: `Bearer ${wiki.token}` } : {}),
|
|
521
|
+
};
|
|
522
|
+
blocks.push({
|
|
523
|
+
name: 'wiki',
|
|
524
|
+
url: String(wiki.url),
|
|
525
|
+
...(Object.keys(headers).length > 0 ? { headers } : {}),
|
|
526
|
+
tools,
|
|
527
|
+
});
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
// External connectors ride along ONLY when the operator declared them safe
|
|
531
|
+
// for the runtime's eyes. A connector added from the serve panel lands here
|
|
532
|
+
// too — without this, exa was offered in chat but the agentic path
|
|
533
|
+
// delegated to the gateway and the Deep Agent answered it had no web tools.
|
|
534
|
+
const EXCLUDED_EXTERNAL_SERVERS = new Set(['cme', 'documents', 'connectors', 'production']);
|
|
535
|
+
for (const [name, entry] of Object.entries(session?.mcp ?? {})) {
|
|
536
|
+
if (!entry?.external || entry.status !== 'connected') continue;
|
|
537
|
+
if (EXCLUDED_EXTERNAL_SERVERS.has(name)) continue;
|
|
538
|
+
if (Array.isArray(entry.requireApproval) && entry.requireApproval.length > 0) continue;
|
|
539
|
+
const tools = (entry.tools ?? [])
|
|
540
|
+
.map((tool) => String(tool.name ?? ''))
|
|
541
|
+
.filter((toolName) => toolName && isReadOnlyExternalTool(toolName));
|
|
542
|
+
if (tools.length === 0) continue;
|
|
543
|
+
blocks.push({
|
|
544
|
+
name,
|
|
545
|
+
url: String(entry.url),
|
|
546
|
+
...(entry.headers && typeof entry.headers === 'object' ? { headers: entry.headers } : {}),
|
|
547
|
+
tools,
|
|
548
|
+
});
|
|
549
|
+
}
|
|
550
|
+
return blocks.length > 0 ? blocks : null;
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
// The Deep Agent's system prompt, built per run from the same ingredients
|
|
554
|
+
// Donna uses: role, the ONE capability being executed (with its declared
|
|
555
|
+
// description), the eyes/bouche/mains boundary, the workspace profile and the
|
|
556
|
+
// reply language. Without it, the runtime falls back to deepagents' generic
|
|
557
|
+
// assistant prompt — which is exactly the "upload your project" hallucination.
|
|
558
|
+
function activeRuntimeSystemPrompt(session, task, assignment) {
|
|
559
|
+
const capability = assignment?.capability ?? null;
|
|
560
|
+
const description = String(capability?.description ?? '').trim();
|
|
561
|
+
const language = session?.language ?? session?.wikircConfig?.language ?? null;
|
|
562
|
+
const profile = loadWorkspaceProfile(session?.workspacePath);
|
|
563
|
+
return [
|
|
564
|
+
'You are the agentic analysis engine of a knowledge workspace (wikiLLM), executed behind the manager Donna.',
|
|
565
|
+
`Execute exactly ONE capability: ${task?.requiredCapability ?? 'unknown'}${description ? ` — ${description}` : ''}.`,
|
|
566
|
+
`Operation: ${task?.operation ?? 'run'}.`,
|
|
567
|
+
'Boundary: you have READ tools only (the workspace wiki). You never modify the workspace — structural changes are proposals you return in your final answer (a planExpansionRequest), the manager integrates them under human approval. Side-effects on the outside world are gated by approval.',
|
|
568
|
+
'Ground every claim in what the read tools return. Never invent pages, names, facts, jobs or results.',
|
|
569
|
+
'Tool discipline: discover real page paths with the list/search tools BEFORE reading. Never guess a path — a read refused for "path not allowed" means the path was invented, so list/search first, then read exactly what exists.',
|
|
570
|
+
...(language ? [`Reply in the workspace language: ${language}.`] : []),
|
|
571
|
+
...(profile ? [`Workspace preferences — apply them to every reply:\n${profile}`] : []),
|
|
572
|
+
].join('\n');
|
|
573
|
+
}
|
|
574
|
+
|
|
181
575
|
function dispatchTaskActivity(session, task, assignment, jobId, statusTool, runId) {
|
|
182
576
|
const activity = normalizeActivity({
|
|
183
577
|
id: jobId,
|
|
@@ -215,7 +609,16 @@ function taskResultFromStatus(task, assignment, jobId, statusPayload, attempt =
|
|
|
215
609
|
status: result.status ?? statusPayload?.status,
|
|
216
610
|
outputRefs: Array.isArray(result.outputRefs) ? result.outputRefs : [],
|
|
217
611
|
metrics: result.metrics ?? {},
|
|
218
|
-
|
|
612
|
+
// The gateway reports its failure at the TOP level of the status payload
|
|
613
|
+
// ({ runId, status, error }), not inside `result`. Reading only
|
|
614
|
+
// `result.error` dropped the only actionable sentence ("no model…") and
|
|
615
|
+
// left Donna to invent a cause.
|
|
616
|
+
error: normalizeTaskError(result.error ?? statusPayload.error),
|
|
617
|
+
// Agent -> DAG (RFC § 30/31/41): a result may request the execution of a
|
|
618
|
+
// deterministic capability. Propagated here so resultAggregator.maybeExpandPlan
|
|
619
|
+
// picks it up and routes it through the manager's own resolution + approval
|
|
620
|
+
// — never a direct scheduler call by the runtime.
|
|
621
|
+
...(result.planExpansionRequest ? { planExpansionRequest: result.planExpansionRequest } : {}),
|
|
219
622
|
rawStatus: statusPayload,
|
|
220
623
|
};
|
|
221
624
|
}
|