@dotdrelle/wiki-manager 0.15.97 → 0.15.99
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/package.json +2 -2
- package/src/agent/graph.js +45 -7
- package/src/agent/graph.test.js +45 -0
- package/src/cli/wiki-manager.js +47 -33
- package/src/commands/slash.js +7 -2
- package/src/contracts/schemas.js +8 -17
- package/src/contracts/schemas.test.js +15 -0
- package/src/core/agentEvents.js +45 -7
- package/src/core/agentEvents.test.js +65 -0
- package/src/core/buildInfo.json +2 -2
- package/src/core/mcp.js +1 -1
- package/src/core/runtimeEventAdapter.js +99 -1
- package/src/core/runtimeEventAdapter.test.js +92 -2
- package/src/core/skillCompiler.test.js +1 -1
- package/src/core/testGate.test.js +33 -0
- package/src/core/toolLoop.js +14 -2
- package/src/core/toolLoop.test.js +28 -0
- package/src/orchestrator/dispatcher.js +19 -0
- package/src/orchestrator/knowledgeSignals.js +260 -0
- package/src/orchestrator/knowledgeSignals.test.js +193 -0
- package/src/orchestrator/proactiveReviewScheduler.js +240 -0
- package/src/orchestrator/proactiveReviewScheduler.test.js +243 -0
- package/src/orchestrator/providers/deepAgentsProvider.js +134 -29
- package/src/orchestrator/providers/deepAgentsProvider.test.js +138 -3
- package/src/orchestrator/resultAggregator.js +115 -1
- package/src/orchestrator/resultAggregator.test.js +138 -0
- package/src/runtime/controlClassify.test.js +31 -0
- package/src/runtime/runner.js +13 -4
- package/src/runtime/runner.test.js +20 -0
- package/src/runtime/server.js +256 -4
- package/src/runtime/server.test.js +13 -1
- package/src/runtime/store.js +1 -1
- package/src/runtime/store.test.js +5 -1
- package/src/shell/openExternal.js +43 -0
- package/src/shell/repl.js +1 -1
- package/wiki-workspace +0 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dotdrelle/wiki-manager",
|
|
3
|
-
"version": "0.15.
|
|
3
|
+
"version": "0.15.99",
|
|
4
4
|
"description": "Agentic shell and orchestration cockpit for llm-wiki workspaces.",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
},
|
|
20
20
|
"scripts": {
|
|
21
21
|
"start": "bun ./bin/wiki-manager.js",
|
|
22
|
-
"test": "node --test src/core/skillInvocation.test.js src/core/skillCompiler.test.js src/runtime/skillRun.test.js src/runtime/controlDrain.test.js src/runtime/controlCancellation.test.js src/cli/runtimeStartup.test.js src/cli/wiki-manager.test.js src/agent/graph.test.js src/agent/skillRecursion.test.js src/contracts/schemas.test.js src/core/activity.test.js src/core/env.test.js src/core/agentsCompose.test.js src/core/profileServiceStatus.test.js src/core/workspaceProfile.test.js src/core/buildInfo.test.js src/core/agentEvents.test.js src/core/skillChainView.test.js src/core/runtimeLog.test.js src/core/runtimeEventAdapter.test.js src/activity/activityAggregator.test.js src/graph/runGraphProjector.test.js src/core/workflow.test.js src/core/planPatch.test.js src/core/agentLoop.test.js src/core/plan.test.js src/core/mcp.test.js src/core/toolLoop.test.js src/core/documentIntake.test.js src/core/dockerCompose.test.js src/core/otherWorkspacesRunning.test.js src/core/wikiSetup.test.js src/core/wikiWorkspace.test.js src/core/wikiWorkspaceStart.test.js src/core/wikirc.test.js src/core/workspaceInherit.test.js src/core/cacert.test.js src/core/composeOverrides.test.js src/core/setEnvValue.test.js src/core/commandFailure.test.js src/core/currentArtifact.test.js src/core/googleGrants.test.js src/core/modelFetch.test.js src/core/startupCheck.test.js src/core/queueStore.test.js src/orchestrator/agentRegistry.test.js src/orchestrator/capabilityRegistry.test.js src/orchestrator/capabilityResolver.test.js src/orchestrator/planValidator.test.js src/orchestrator/planIntegrator.test.js src/orchestrator/taskStatuses.test.js src/orchestrator/scheduler.test.js src/orchestrator/attemptManager.test.js src/orchestrator/resultAggregator.test.js src/orchestrator/approvalPolicy.test.js src/orchestrator/dispatcher.test.js src/orchestrator/objectiveResolver.test.js src/orchestrator/providers/fakeRuntimeProvider.test.js src/orchestrator/providers/runtimeProviders.test.js src/orchestrator/providers/dispatcherExternalRuntime.test.js src/orchestrator/providers/deepAgentsProvider.test.js src/commands/slash.test.js src/shell/repl.test.js src/shell/setupWizardModality.test.js src/shell/setupWizardPlaceholders.test.js src/shell/setupWizardSuggestions.test.js src/shell/setupWizardDiscovery.test.js src/shell/wrapText.test.js src/runtime/lifecycle.test.js src/runtime/store.test.js src/runtime/workspaceIsolation.test.js src/runtime/controlMessages.test.js src/runtime/deltaCoalescer.test.js src/runtime/recoveryManager.test.js src/runtime/server.test.js src/runtime/supervisor.test.js src/runtime/delegation.test.js src/runtime/runner.test.js src/runtime/runner.e2e.test.js src/runtime/skillChain.e2e.test.js src/runtime/donna-contract.test.js src/runtime/approvals.test.js src/runtime/auth.test.js src/runtime/totp.test.js src/runtime/loginSession.test.js src/runtime/loginRoutes.test.js",
|
|
22
|
+
"test": "node --test src/core/skillInvocation.test.js src/core/skillCompiler.test.js src/runtime/skillRun.test.js src/runtime/controlDrain.test.js src/runtime/controlCancellation.test.js src/cli/runtimeStartup.test.js src/cli/wiki-manager.test.js src/agent/graph.test.js src/agent/skillRecursion.test.js src/contracts/schemas.test.js src/core/activity.test.js src/core/env.test.js src/core/agentsCompose.test.js src/core/profileServiceStatus.test.js src/core/workspaceProfile.test.js src/core/buildInfo.test.js src/core/testGate.test.js src/core/agentEvents.test.js src/core/skillChainView.test.js src/core/runtimeLog.test.js src/core/runtimeEventAdapter.test.js src/activity/activityAggregator.test.js src/graph/runGraphProjector.test.js src/core/workflow.test.js src/core/planPatch.test.js src/core/agentLoop.test.js src/core/plan.test.js src/core/mcp.test.js src/core/mcpEndpoints.test.js src/core/toolLoop.test.js src/core/documentIntake.test.js src/core/dockerCompose.test.js src/core/otherWorkspacesRunning.test.js src/core/wikiSetup.test.js src/core/wikiWorkspace.test.js src/core/wikiWorkspaceStart.test.js src/core/wikirc.test.js src/core/workspaceInherit.test.js src/core/cacert.test.js src/core/composeOverrides.test.js src/core/setEnvValue.test.js src/core/commandFailure.test.js src/core/currentArtifact.test.js src/core/googleGrants.test.js src/core/modelFetch.test.js src/core/startupCheck.test.js src/core/queueStore.test.js src/orchestrator/agentRegistry.test.js src/orchestrator/capabilityRegistry.test.js src/orchestrator/capabilityResolver.test.js src/orchestrator/planValidator.test.js src/orchestrator/planIntegrator.test.js src/orchestrator/taskStatuses.test.js src/orchestrator/scheduler.test.js src/orchestrator/attemptManager.test.js src/orchestrator/resultAggregator.test.js src/orchestrator/proactiveReviewScheduler.test.js src/orchestrator/knowledgeSignals.test.js src/orchestrator/approvalPolicy.test.js src/orchestrator/dispatcher.test.js src/orchestrator/objectiveResolver.test.js src/orchestrator/providers/fakeRuntimeProvider.test.js src/orchestrator/providers/runtimeProviders.test.js src/orchestrator/providers/dispatcherExternalRuntime.test.js src/orchestrator/providers/deepAgentsProvider.test.js src/commands/slash.test.js src/shell/repl.test.js src/shell/setupWizardModality.test.js src/shell/setupWizardPlaceholders.test.js src/shell/setupWizardSuggestions.test.js src/shell/setupWizardDiscovery.test.js src/shell/wrapText.test.js src/runtime/lifecycle.test.js src/runtime/store.test.js src/runtime/workspaceIsolation.test.js src/runtime/controlMessages.test.js src/runtime/controlClassify.test.js src/runtime/deltaCoalescer.test.js src/runtime/recoveryManager.test.js src/runtime/server.test.js src/runtime/supervisor.test.js src/runtime/delegation.test.js src/runtime/runner.test.js src/runtime/runner.e2e.test.js src/runtime/skillChain.e2e.test.js src/runtime/donna-contract.test.js src/runtime/approvals.test.js src/runtime/auth.test.js src/runtime/totp.test.js src/runtime/loginSession.test.js src/runtime/loginRoutes.test.js",
|
|
23
23
|
"check-versions": "node scripts/check-versions.js",
|
|
24
24
|
"prepack": "node scripts/check-versions.js",
|
|
25
25
|
"prepublishOnly": "node scripts/check-versions.js",
|
package/src/agent/graph.js
CHANGED
|
@@ -1905,7 +1905,11 @@ export function createAgentGraph(options = {}) {
|
|
|
1905
1905
|
let terminalFailure = null;
|
|
1906
1906
|
let skillLaunch = null;
|
|
1907
1907
|
|
|
1908
|
-
|
|
1908
|
+
// The index is kept so a break can name the calls that never ran: every
|
|
1909
|
+
// iteration pushes a tool result before it continues or completes, so
|
|
1910
|
+
// everything AFTER the break is exactly the unexecuted set.
|
|
1911
|
+
let stoppedAt = toolCalls.length;
|
|
1912
|
+
for (const [callIndex, call] of toolCalls.entries()) {
|
|
1909
1913
|
const resolved = resolveToolCallName(state.session.mcp, call.function.name, INTERNAL_TOOL_SERVERS);
|
|
1910
1914
|
const { server, tool } = resolved;
|
|
1911
1915
|
const argsSummary = summarizeToolArguments(call.function.arguments);
|
|
@@ -2031,10 +2035,17 @@ export function createAgentGraph(options = {}) {
|
|
|
2031
2035
|
),
|
|
2032
2036
|
objectives: Number(skillResult.objectiveCount ?? skillResult.objectives ?? 1) || 1,
|
|
2033
2037
|
};
|
|
2034
|
-
} else if (skillResult?.ok === false) {
|
|
2035
|
-
// A recoverable refusal (guessed skill
|
|
2036
|
-
// turn alive so the model can correct itself or
|
|
2037
|
-
// not let the progress note call it a success.
|
|
2038
|
+
} else if (skillResult?.ok === false && skillResult?.needsInput !== true) {
|
|
2039
|
+
// A recoverable refusal (a guessed skill that does not exist):
|
|
2040
|
+
// keep the turn alive so the model can correct itself or
|
|
2041
|
+
// delegate, but do not let the progress note call it a success.
|
|
2042
|
+
//
|
|
2043
|
+
// `needsInput` is excluded on purpose: "ask the user for the
|
|
2044
|
+
// missing scope" is a conversational blocker, and marking it
|
|
2045
|
+
// failed published `runtime__run_skill failed: {…}` into the
|
|
2046
|
+
// progress surfaces while the turn was working exactly as
|
|
2047
|
+
// intended. The `delegate` branch below already treats the
|
|
2048
|
+
// identical case that way; the two must not disagree.
|
|
2038
2049
|
ok = false;
|
|
2039
2050
|
}
|
|
2040
2051
|
}
|
|
@@ -2186,7 +2197,34 @@ export function createAgentGraph(options = {}) {
|
|
|
2186
2197
|
tool_call_id: call.id,
|
|
2187
2198
|
content: boundedResult,
|
|
2188
2199
|
});
|
|
2189
|
-
if (terminalFailure) break;
|
|
2200
|
+
if (terminalFailure || skillLaunch) { stoppedAt = callIndex + 1; break; }
|
|
2201
|
+
}
|
|
2202
|
+
|
|
2203
|
+
// A skill launch owns execution and ends the turn, so the rest of the
|
|
2204
|
+
// batch is NOT executed — a companion runtime__delegate in the same batch
|
|
2205
|
+
// used to start a second, independent run with nothing in the thread
|
|
2206
|
+
// naming it. Every unexecuted call still gets its tool result: a provider
|
|
2207
|
+
// that sees tool_calls without matching results on a replayed history
|
|
2208
|
+
// rejects the conversation.
|
|
2209
|
+
if (skillLaunch) {
|
|
2210
|
+
const dropped = toolCalls.slice(stoppedAt);
|
|
2211
|
+
const notRun = `Not executed: ${skillLaunch.publicInput} was launched earlier in this `
|
|
2212
|
+
+ 'turn and owns execution from here. Do not start a second run for the same objective.';
|
|
2213
|
+
for (const call of dropped) {
|
|
2214
|
+
emitAgentEvent(state.session, 'tool_call_result', 'tool', {
|
|
2215
|
+
callId: call.id,
|
|
2216
|
+
name: call.function?.name ?? 'tool',
|
|
2217
|
+
ok: false,
|
|
2218
|
+
result: notRun,
|
|
2219
|
+
summary: 'skipped',
|
|
2220
|
+
});
|
|
2221
|
+
toolResultMessages.push({ role: 'tool', tool_call_id: call.id, content: notRun });
|
|
2222
|
+
}
|
|
2223
|
+
if (dropped.length > 0) {
|
|
2224
|
+
state.session._onStep?.(
|
|
2225
|
+
`${dropped.length} tool call(s) skipped: ${skillLaunch.publicInput} owns execution`,
|
|
2226
|
+
);
|
|
2227
|
+
}
|
|
2190
2228
|
}
|
|
2191
2229
|
|
|
2192
2230
|
if (terminalFailure) {
|
|
@@ -2205,7 +2243,7 @@ export function createAgentGraph(options = {}) {
|
|
|
2205
2243
|
// language like the `/turn` skill path. No further model turn: the skill
|
|
2206
2244
|
// is launched and owns execution, so nothing can contradict it.
|
|
2207
2245
|
const response = await generateSkillAcknowledgment(state.session, skillLaunch).catch(
|
|
2208
|
-
() => `Started ${skillLaunch.publicInput} — ${skillLaunch.objectives} step(s)
|
|
2246
|
+
() => `Started ${skillLaunch.publicInput} — ${skillLaunch.objectives} step(s) queued.`,
|
|
2209
2247
|
);
|
|
2210
2248
|
return {
|
|
2211
2249
|
messages: toolResultMessages,
|
package/src/agent/graph.test.js
CHANGED
|
@@ -678,6 +678,14 @@ test('a natural-language skill match cannot drop declared scope and fall back to
|
|
|
678
678
|
const toolResult = session.agentEvents.find((event) => event.type === 'tool_call_result');
|
|
679
679
|
assert.match(toolResult?.payload?.result ?? '', /missingParameters/);
|
|
680
680
|
assert.match(toolResult?.payload?.result ?? '', /never replace a missing parameter with an unscoped/);
|
|
681
|
+
// "Ask the user for the missing scope" is a conversational blocker, not an
|
|
682
|
+
// execution failure — the turn is working exactly as intended. Marking it
|
|
683
|
+
// failed published `runtime__run_skill failed: {…}` into the progress and
|
|
684
|
+
// activity surfaces; the `delegate` branch handles the identical case as a
|
|
685
|
+
// non-failure, and the two must not disagree.
|
|
686
|
+
assert.equal(toolResult?.payload?.ok, true);
|
|
687
|
+
const progress = session.agentEvents.find((event) => event.type === 'assistant_progress');
|
|
688
|
+
assert.doesNotMatch(progress?.payload?.message ?? '', /failed/i);
|
|
681
689
|
} finally {
|
|
682
690
|
rmSync(root, { recursive: true, force: true });
|
|
683
691
|
}
|
|
@@ -776,6 +784,43 @@ test('a recoverable skill refusal lets the delegate fallback run in the same tur
|
|
|
776
784
|
assert.notEqual(result.terminalToolFailure, true);
|
|
777
785
|
});
|
|
778
786
|
|
|
787
|
+
test('an ACCEPTED skill launch stops the rest of the batch, and says what it dropped', async () => {
|
|
788
|
+
// The mirror of the test above: a refusal must let the fallback run, an
|
|
789
|
+
// acceptance must not. The skill owns execution from there, but the loop
|
|
790
|
+
// kept going — a companion runtime__delegate in the same batch started a
|
|
791
|
+
// second, independent run while only the skill was acknowledged, with
|
|
792
|
+
// nothing in the thread naming the delegated one.
|
|
793
|
+
let delegated = false;
|
|
794
|
+
const session = sessionBase({
|
|
795
|
+
runtime: { url: 'http://runtime.test' },
|
|
796
|
+
_runSkillWithinRun: async () => ({ accepted: true, skill: 'pipeline', objectiveCount: 1 }),
|
|
797
|
+
_delegateWithinRun: async () => { delegated = true; return { runId: 'run-2', summary: { tasks: 1 } }; },
|
|
798
|
+
llm: {
|
|
799
|
+
async completeWithTools({ tools }) {
|
|
800
|
+
if (tools.some((tool) => tool.function?.name === 'classify_action_request')) {
|
|
801
|
+
return { content: null, message: { role: 'assistant', content: null }, tool_calls: [{ id: 'classify', type: 'function', function: { name: 'classify_action_request', arguments: '{"action":true}' } }] };
|
|
802
|
+
}
|
|
803
|
+
return {
|
|
804
|
+
content: null, message: { role: 'assistant', content: null },
|
|
805
|
+
tool_calls: [
|
|
806
|
+
{ id: 'skill', type: 'function', function: { name: 'runtime__run_skill', arguments: '{"skillName":"pipeline","selectionKind":"explicit_name"}' } },
|
|
807
|
+
{ id: 'also', type: 'function', function: { name: 'runtime__delegate', arguments: '{"objective":"construire le livrable"}' } },
|
|
808
|
+
],
|
|
809
|
+
};
|
|
810
|
+
},
|
|
811
|
+
},
|
|
812
|
+
});
|
|
813
|
+
await createAgentGraph().invoke({ input: '/pipeline', session });
|
|
814
|
+
assert.equal(delegated, false, 'the delegate must not start a second run behind the skill');
|
|
815
|
+
// …and the skipped call is announced rather than silently dropped, with a
|
|
816
|
+
// tool result so a replayed history is not missing one.
|
|
817
|
+
const skipped = session.agentEvents.find(
|
|
818
|
+
(event) => event.type === 'tool_call_result' && event.payload?.callId === 'also',
|
|
819
|
+
);
|
|
820
|
+
assert.equal(skipped?.payload?.summary, 'skipped');
|
|
821
|
+
assert.match(skipped?.payload?.result ?? '', /owns execution/);
|
|
822
|
+
});
|
|
823
|
+
|
|
779
824
|
test('tool argument normalization repairs only an unambiguous schema-compatible field name', () => {
|
|
780
825
|
const schema = {
|
|
781
826
|
type: 'object',
|
package/src/cli/wiki-manager.js
CHANGED
|
@@ -1725,6 +1725,13 @@ async function runRuntime(argv, agent) {
|
|
|
1725
1725
|
async function executeInteractiveTurn(context, body, { signal, turnId } = {}) {
|
|
1726
1726
|
const input = String(body.input ?? body.prompt ?? '').trim();
|
|
1727
1727
|
if (!input) throw new Error('Missing input.');
|
|
1728
|
+
// The reader's own words, when the caller augmented `input` with system
|
|
1729
|
+
// facts (a status question gets the runtime's fact block appended for the
|
|
1730
|
+
// model). The thread, the SSE stream and the replayed history must all
|
|
1731
|
+
// show what was typed — publishing the fact block as the user's message
|
|
1732
|
+
// put a raw English dump in their bubble and seeded every later turn with
|
|
1733
|
+
// it.
|
|
1734
|
+
const displayInput = String(body.displayInput ?? '').trim() || input;
|
|
1728
1735
|
// The runtime may start while optional agents are still stopped. `/start
|
|
1729
1736
|
// agents` happens in the shell process, so its refreshed MCP snapshot does
|
|
1730
1737
|
// not mutate this long-lived runtime context. Re-probe only while at least
|
|
@@ -1776,7 +1783,7 @@ async function runRuntime(argv, agent) {
|
|
|
1776
1783
|
origin: 'runtime_turn',
|
|
1777
1784
|
turnId,
|
|
1778
1785
|
workspace: context.workspace ?? null,
|
|
1779
|
-
payload: { content:
|
|
1786
|
+
payload: { content: displayInput },
|
|
1780
1787
|
}));
|
|
1781
1788
|
// Read-only chat turn: same chatAccess policy as the Shell UI's /chat, now
|
|
1782
1789
|
// reachable over HTTP so `wiki serve` chat mode gets read tools without
|
|
@@ -1792,39 +1799,46 @@ async function runRuntime(argv, agent) {
|
|
|
1792
1799
|
body.context?.openWikiPages ?? body.context?.openWikiPage,
|
|
1793
1800
|
);
|
|
1794
1801
|
let response;
|
|
1795
|
-
|
|
1796
|
-
|
|
1797
|
-
|
|
1798
|
-
|
|
1799
|
-
|
|
1800
|
-
|
|
1801
|
-
|
|
1802
|
-
|
|
1803
|
-
|
|
1804
|
-
|
|
1805
|
-
|
|
1806
|
-
|
|
1807
|
-
|
|
1808
|
-
|
|
1809
|
-
|
|
1810
|
-
|
|
1811
|
-
|
|
1812
|
-
|
|
1813
|
-
|
|
1814
|
-
|
|
1815
|
-
|
|
1816
|
-
|
|
1817
|
-
|
|
1818
|
-
|
|
1819
|
-
|
|
1820
|
-
|
|
1821
|
-
|
|
1822
|
-
|
|
1823
|
-
|
|
1802
|
+
try {
|
|
1803
|
+
if (chatMode) {
|
|
1804
|
+
ephemeral.chatMode = true;
|
|
1805
|
+
ephemeral.chatAccess = readChatAccessConfig();
|
|
1806
|
+
const history = messages.length && messages[messages.length - 1]?.role === 'user'
|
|
1807
|
+
? messages.slice(0, -1)
|
|
1808
|
+
: messages;
|
|
1809
|
+
response = await runHeadlessChatTurn(ephemeral, input, {
|
|
1810
|
+
history,
|
|
1811
|
+
onStep: ephemeral._onStep,
|
|
1812
|
+
// Fragments de réponse publiés au fil de l'eau, coalescés (voir
|
|
1813
|
+
// deltaCoalescer ci-dessus). Le réducteur les agrège dans la dernière
|
|
1814
|
+
// entrée de conversation (`assistant_delta`), que `assistant_message`
|
|
1815
|
+
// vient ensuite figer : les deux interfaces voient la réponse s'écrire
|
|
1816
|
+
// sans qu'un insert SQLite par token ne bloque le flux.
|
|
1817
|
+
onTextDelta: (delta) => deltaCoalescer.push(delta),
|
|
1818
|
+
onTextReset: () => {
|
|
1819
|
+
deltaCoalescer.reset();
|
|
1820
|
+
dispatchAgentEvent(ephemeral, createAgentEvent('assistant_delta_reset', {
|
|
1821
|
+
origin: 'runtime_turn',
|
|
1822
|
+
turnId,
|
|
1823
|
+
workspace: context.workspace ?? null,
|
|
1824
|
+
payload: {},
|
|
1825
|
+
}));
|
|
1826
|
+
},
|
|
1827
|
+
openWikiPages,
|
|
1828
|
+
});
|
|
1829
|
+
} else {
|
|
1830
|
+
ephemeral.openWikiPages = openWikiPages;
|
|
1831
|
+
response = await runAgentTurn(agent, ephemeral, input, { messages, signal });
|
|
1832
|
+
}
|
|
1833
|
+
} finally {
|
|
1834
|
+
// In a `finally`, not after the await: a throwing or aborted turn left
|
|
1835
|
+
// the 80 ms timer armed, so a stray assistant_delta fired AFTER the
|
|
1836
|
+
// "Runtime turn failed" message and appended orphan fragments to the
|
|
1837
|
+
// wrong conversation entry — and the handle kept the session closure
|
|
1838
|
+
// alive. Flush the tail first so ordering survives either way.
|
|
1839
|
+
deltaCoalescer.flush();
|
|
1840
|
+
deltaCoalescer.dispose();
|
|
1824
1841
|
}
|
|
1825
|
-
// Flush the tail before the turn is finalized, then stop the timer.
|
|
1826
|
-
deltaCoalescer.flush();
|
|
1827
|
-
deltaCoalescer.dispose();
|
|
1828
1842
|
// Persist the artifact the turn may have opened/edited (template_write,
|
|
1829
1843
|
// template_read, …) back onto the long-lived session, so the next /turn —
|
|
1830
1844
|
// chat or agent — sees it. The ephemeral session is otherwise discarded.
|
package/src/commands/slash.js
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
*/
|
|
10
10
|
import { isTerminal } from '../orchestrator/taskStatuses.js';
|
|
11
11
|
import { existsSync, readdirSync, readFileSync, statSync } from 'node:fs';
|
|
12
|
-
import { openExternalUrl } from '../shell/openExternal.js';
|
|
12
|
+
import { openAppWindowUrl, openExternalUrl } from '../shell/openExternal.js';
|
|
13
13
|
import { classifyCommandFailure, failureHint, rawFailureText } from '../core/commandFailure.js';
|
|
14
14
|
import { basename, join, relative } from 'node:path';
|
|
15
15
|
import { composeServices, listServices, otherWorkspacesRunning, runWikiCli, serviceLogs, serviceNames, serviceStates, startService, stopService } from '../core/compose.js';
|
|
@@ -845,7 +845,7 @@ ${helpPair('/upload <path>', 'Upload document', '/uploads', 'Uploaded docs')}
|
|
|
845
845
|
${helpPair('/upload convert pending', 'Convert pending', '/uploads clean', 'Clean uploads')}
|
|
846
846
|
${helpPair('/wiki', 'Run wiki index', '/wiki run <args>', 'Raw wiki CLI')}
|
|
847
847
|
${helpPair('/chat', 'Chat mode', '/agent [question]', 'Agent mode / one-shot')}
|
|
848
|
-
${helpPair('/openui', 'Open web UI
|
|
848
|
+
${helpPair('/openui', 'Open web UI as a desktop window', '', '')}
|
|
849
849
|
${helpPair('/run status', 'Runtime status', '/run kill', 'Kill runtime run(s)')}
|
|
850
850
|
${helpPair('/run capability <id>', 'Deterministic capability run', '/approve', 'Grant pending approval')}
|
|
851
851
|
${helpPair('/cancel', 'Cancel active run', '', '')}
|
|
@@ -1770,6 +1770,11 @@ export async function handleSlashCommand(line, context) {
|
|
|
1770
1770
|
// No readable session (gate off, or a custom state dir): plain URL.
|
|
1771
1771
|
}
|
|
1772
1772
|
const note = context.session.workspaceEnv ? '' : ' (no workspace loaded — using default port)';
|
|
1773
|
+
// App-mode first: a chromeless Chrome/Edge window picks up serve's own
|
|
1774
|
+
// manifest (name, icon, window-controls-overlay) with no prior "Install"
|
|
1775
|
+
// step. Falls back to the plain default-browser tab when neither is
|
|
1776
|
+
// found (Safari/Firefox-only machines).
|
|
1777
|
+
if (openAppWindowUrl(openUrl)) return { output: `Opening web UI: ${url}${note}` };
|
|
1773
1778
|
if (openExternalUrl(openUrl)) return { output: `Opening web UI: ${url}${note}` };
|
|
1774
1779
|
return { output: `Web UI: ${url}${note}` };
|
|
1775
1780
|
}
|
package/src/contracts/schemas.js
CHANGED
|
@@ -369,23 +369,14 @@ const runtimeEventSchema = {
|
|
|
369
369
|
required: ['type'],
|
|
370
370
|
additionalProperties: true,
|
|
371
371
|
properties: {
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
'subagent_started',
|
|
381
|
-
'subagent_finished',
|
|
382
|
-
'message',
|
|
383
|
-
'approval_required',
|
|
384
|
-
'run_completed',
|
|
385
|
-
'run_failed',
|
|
386
|
-
'run_cancelled',
|
|
387
|
-
],
|
|
388
|
-
},
|
|
372
|
+
// Deliberately OPEN: a gateway newer than the manager emits event types
|
|
373
|
+
// this version has never heard of, and a closed enum here threw inside
|
|
374
|
+
// `normalizeRuntimeEvent` before `runtimeEventAdapter` could decide what to
|
|
375
|
+
// do with them — the provider's catch swallowed the frame, so the whole
|
|
376
|
+
// activity contract was invisible in production while its unit tests
|
|
377
|
+
// passed. The adapter owns the vocabulary and journals an unknown type;
|
|
378
|
+
// the schema must let it see one.
|
|
379
|
+
type: { type: 'string' },
|
|
389
380
|
runId: { type: 'string' },
|
|
390
381
|
tool: { type: 'string' },
|
|
391
382
|
durationMs: { type: 'number' },
|
|
@@ -250,3 +250,18 @@ test('capability status contract carries dynamic pending inputs without prescrib
|
|
|
250
250
|
assert.equal(validateContract('capabilityStatus', status).ok, true);
|
|
251
251
|
assert.equal(validateContract('capabilityStatus', { ...status, pendingInputs: [{ type: 'file' }] }).ok, false);
|
|
252
252
|
});
|
|
253
|
+
|
|
254
|
+
test('the runtime event contract tolerates a type this version does not know', () => {
|
|
255
|
+
// A newer gateway emits types this manager has never heard of. A closed enum
|
|
256
|
+
// made `normalizeRuntimeEvent` throw before the adapter could journal it, so
|
|
257
|
+
// the whole activity contract was silently invisible. The schema must let an
|
|
258
|
+
// unknown type through; deciding what to do with it belongs to the adapter.
|
|
259
|
+
for (const type of ['phase_started', 'progress', 'heartbeat', 'finding', 'degraded', 'notice', 'stream_epoch', 'a_future_type']) {
|
|
260
|
+
assert.equal(
|
|
261
|
+
validateContract('runtimeEvent', { type, runId: 'r1' }).ok,
|
|
262
|
+
true,
|
|
263
|
+
`${type} must be accepted`,
|
|
264
|
+
);
|
|
265
|
+
}
|
|
266
|
+
assert.equal(validateContract('runtimeEvent', { runId: 'r1' }).ok, false, 'a missing type is still refused');
|
|
267
|
+
});
|
package/src/core/agentEvents.js
CHANGED
|
@@ -231,6 +231,10 @@ function createProjectionState() {
|
|
|
231
231
|
agents: {},
|
|
232
232
|
summary: null,
|
|
233
233
|
status: 'idle',
|
|
234
|
+
// Liveness from the external runtime's heartbeat (lot 2). Display-only:
|
|
235
|
+
// never persisted, never in the conversation.
|
|
236
|
+
lastHeartbeatAt: null,
|
|
237
|
+
lastHeartbeatElapsedMs: 0,
|
|
234
238
|
};
|
|
235
239
|
}
|
|
236
240
|
|
|
@@ -263,6 +267,8 @@ function publicProjection(state) {
|
|
|
263
267
|
.sort((a, b) => a.agentInstanceId.localeCompare(b.agentInstanceId)),
|
|
264
268
|
summary: state.summary,
|
|
265
269
|
status: state.status,
|
|
270
|
+
lastHeartbeatAt: state.lastHeartbeatAt ?? null,
|
|
271
|
+
lastHeartbeatElapsedMs: state.lastHeartbeatElapsedMs ?? 0,
|
|
266
272
|
};
|
|
267
273
|
return {
|
|
268
274
|
...projection,
|
|
@@ -292,6 +298,21 @@ export function applyAgentProjectionToSession(session, projection) {
|
|
|
292
298
|
} : session.productionActivity ?? null;
|
|
293
299
|
}
|
|
294
300
|
|
|
301
|
+
/**
|
|
302
|
+
* Clears the `pending_approval` latch once no approval is outstanding.
|
|
303
|
+
*
|
|
304
|
+
* Called after EVERY approval decision, granted or rejected. Only `granted`
|
|
305
|
+
* used to clear it, so a refusal left the projection reporting
|
|
306
|
+
* `pending_approval` for the rest of the run: both UIs kept asking for a
|
|
307
|
+
* decision already made, while `explainControlState` found nothing pending and
|
|
308
|
+
* answered "run is active". One function so the two verdicts cannot drift.
|
|
309
|
+
*/
|
|
310
|
+
function releaseApprovalLatch(state) {
|
|
311
|
+
if (state.status !== 'pending_approval') return;
|
|
312
|
+
if ((state.approvals ?? []).some((approval) => approval.status === 'pending_approval')) return;
|
|
313
|
+
state.status = 'running';
|
|
314
|
+
}
|
|
315
|
+
|
|
295
316
|
function hasRunningPlanStep(state) {
|
|
296
317
|
return (Array.isArray(state.plan) ? state.plan : [])
|
|
297
318
|
.some((step) => isActive(step?.status));
|
|
@@ -318,8 +339,17 @@ function applyEvent(state, event) {
|
|
|
318
339
|
state.planPatches = [];
|
|
319
340
|
state.summary = null;
|
|
320
341
|
state.subagents = [];
|
|
342
|
+
state.lastHeartbeatAt = null;
|
|
343
|
+
state.lastHeartbeatElapsedMs = 0;
|
|
321
344
|
pruneTerminalControlItems(state.controlQueue);
|
|
322
345
|
return;
|
|
346
|
+
case 'runtime_heartbeat':
|
|
347
|
+
// Liveness only: the external runtime saying "still working" during a
|
|
348
|
+
// long, tool-less phase. A timestamp the run strip reads; never a
|
|
349
|
+
// conversation entry, never persisted (store.js NON_PERSISTED_EVENT_TYPES).
|
|
350
|
+
state.lastHeartbeatAt = event.ts ?? new Date().toISOString();
|
|
351
|
+
state.lastHeartbeatElapsedMs = Number(event.payload?.elapsedMs) || 0;
|
|
352
|
+
return;
|
|
323
353
|
case 'user_message':
|
|
324
354
|
state.conversation.push({ role: 'user', content: String(event.payload?.content ?? '') });
|
|
325
355
|
return;
|
|
@@ -450,6 +480,15 @@ function applyEvent(state, event) {
|
|
|
450
480
|
case 'task.failed':
|
|
451
481
|
appendLog(state, taskLogLine(state, event, 'failed'));
|
|
452
482
|
return;
|
|
483
|
+
// Stable business facts: a workspace's knowledge changed. They are
|
|
484
|
+
// published for the proactive scheduler and shown as one journal line.
|
|
485
|
+
case 'knowledge.ingested':
|
|
486
|
+
case 'knowledge.rebuilt': {
|
|
487
|
+
const workspace = String(event.payload?.workspace ?? 'workspace');
|
|
488
|
+
const version = String(event.payload?.sourceVersion ?? 'unknown version');
|
|
489
|
+
appendLog(state, `${logTime(event.ts)} ${event.type} — ${workspace} (${version})`.trim());
|
|
490
|
+
return;
|
|
491
|
+
}
|
|
453
492
|
case 'plan.revision_changed':
|
|
454
493
|
if (Array.isArray(event.payload?.tasks)) {
|
|
455
494
|
state.plan = normalizePlan(event.payload.tasks, { owner: 'orchestrator', planRevision: state.planRevision });
|
|
@@ -629,13 +668,7 @@ function applyEvent(state, event) {
|
|
|
629
668
|
};
|
|
630
669
|
upsertApproval(state, grant);
|
|
631
670
|
markCoveredApprovalsApproved(state.approvals, grant, event.ts);
|
|
632
|
-
|
|
633
|
-
// approval is still outstanding (a run-scoped grant clears its covered
|
|
634
|
-
// ones, markCoveredApprovalsApproved above).
|
|
635
|
-
if (state.status === 'pending_approval'
|
|
636
|
-
&& !(state.approvals ?? []).some((approval) => approval.status === 'pending_approval')) {
|
|
637
|
-
state.status = 'running';
|
|
638
|
-
}
|
|
671
|
+
releaseApprovalLatch(state);
|
|
639
672
|
return;
|
|
640
673
|
}
|
|
641
674
|
case 'approval.rejected':
|
|
@@ -654,6 +687,7 @@ function applyEvent(state, event) {
|
|
|
654
687
|
reason: event.payload?.reason ?? null,
|
|
655
688
|
rejectedAt: event.ts,
|
|
656
689
|
});
|
|
690
|
+
releaseApprovalLatch(state);
|
|
657
691
|
return;
|
|
658
692
|
case 'run_done':
|
|
659
693
|
state.status = 'done';
|
|
@@ -720,6 +754,10 @@ function applyEvent(state, event) {
|
|
|
720
754
|
: {}),
|
|
721
755
|
...(event.payload?.selectionKind ? { selectionKind: event.payload.selectionKind } : {}),
|
|
722
756
|
...(Number.isInteger(event.payload?.chainSequence) ? { chainSequence: event.payload.chainSequence } : {}),
|
|
757
|
+
// A proactive review's identity must survive projection and replay: the
|
|
758
|
+
// drain hands it back to the run it starts, which is what lets the
|
|
759
|
+
// result be filed as a review rather than lost as an anonymous audit.
|
|
760
|
+
...(event.payload?.proactiveReview ? { proactiveReview: event.payload.proactiveReview } : {}),
|
|
723
761
|
optional: event.payload?.optional === true,
|
|
724
762
|
continueOnFailure: event.payload?.continueOnFailure === true,
|
|
725
763
|
});
|
|
@@ -151,6 +151,31 @@ test('reduceAgentEvents: granting the approval puts the run back to running', ()
|
|
|
151
151
|
assert.equal(projection.status, 'running');
|
|
152
152
|
});
|
|
153
153
|
|
|
154
|
+
test('reduceAgentEvents: REJECTING the approval also puts the run back to running', () => {
|
|
155
|
+
// A refusal is a decision. Only `granted` cleared the latch, so a rejected
|
|
156
|
+
// approval left both UIs asking for a decision already made, for the rest of
|
|
157
|
+
// the run.
|
|
158
|
+
const projection = reduceAgentEvents([
|
|
159
|
+
createAgentEvent('run_started', { origin: 'runtime' }),
|
|
160
|
+
createAgentEvent('plan_set', { origin: 'tool', payload: { steps: ['Rebuild the concepts'] } }),
|
|
161
|
+
createAgentEvent('plan_step_updated', { origin: 'runtime', payload: { step: 1, status: 'waiting_approval' } }),
|
|
162
|
+
createAgentEvent('approval.requested', { origin: 'runtime', payload: { id: 'a1', scope: 'task', taskId: 't1' } }),
|
|
163
|
+
createAgentEvent('approval.rejected', { origin: 'runtime', payload: { id: 'a1', scope: 'task', taskId: 't1' } }),
|
|
164
|
+
]);
|
|
165
|
+
assert.equal(projection.status, 'running');
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
test('reduceAgentEvents: rejecting one of two approvals keeps the run blocked', () => {
|
|
169
|
+
const projection = reduceAgentEvents([
|
|
170
|
+
createAgentEvent('run_started', { origin: 'runtime' }),
|
|
171
|
+
createAgentEvent('plan_set', { origin: 'tool', payload: { steps: ['Export', 'Build'] } }),
|
|
172
|
+
createAgentEvent('approval.requested', { origin: 'runtime', payload: { id: 'a1', scope: 'task', taskId: 't1' } }),
|
|
173
|
+
createAgentEvent('approval.requested', { origin: 'runtime', payload: { id: 'a2', scope: 'task', taskId: 't2' } }),
|
|
174
|
+
createAgentEvent('approval.rejected', { origin: 'runtime', payload: { id: 'a1', scope: 'task', taskId: 't1' } }),
|
|
175
|
+
]);
|
|
176
|
+
assert.equal(projection.status, 'pending_approval');
|
|
177
|
+
});
|
|
178
|
+
|
|
154
179
|
test('reduceAgentEvents: an approval request does not hide a genuinely running task', () => {
|
|
155
180
|
const projection = reduceAgentEvents([
|
|
156
181
|
createAgentEvent('run_started', { origin: 'runtime' }),
|
|
@@ -874,3 +899,43 @@ test('subagent_started/finished track the collective timeline, reset per run', (
|
|
|
874
899
|
dispatchAgentEvent(session, createAgentEvent('run_started', { origin: 'runtime', runId: 'r2', payload: {} }));
|
|
875
900
|
assert.equal(session.agentProjection.subagents.length, 0, 'a new run starts a fresh timeline');
|
|
876
901
|
});
|
|
902
|
+
|
|
903
|
+
test('a runtime heartbeat sets liveness only, and a new run clears it', () => {
|
|
904
|
+
const session = {};
|
|
905
|
+
dispatchAgentEvent(session, createAgentEvent('run_started', { origin: 'runtime', runId: 'r1', payload: {} }));
|
|
906
|
+
dispatchAgentEvent(session, createAgentEvent('runtime_heartbeat', {
|
|
907
|
+
origin: 'runtime_provider', runId: 'r1', payload: { elapsedMs: 30_000 },
|
|
908
|
+
}));
|
|
909
|
+
|
|
910
|
+
assert.ok(session.agentProjection.lastHeartbeatAt, 'the beat is visible to the strip');
|
|
911
|
+
assert.equal(session.agentProjection.lastHeartbeatElapsedMs, 30_000);
|
|
912
|
+
// A heartbeat is not an event the conversation projection can seed from.
|
|
913
|
+
assert.equal(session.agentProjection.conversation.length, 0);
|
|
914
|
+
|
|
915
|
+
dispatchAgentEvent(session, createAgentEvent('run_started', { origin: 'runtime', runId: 'r2', payload: {} }));
|
|
916
|
+
assert.equal(session.agentProjection.lastHeartbeatAt, null, 'a new run starts with no stale beat');
|
|
917
|
+
});
|
|
918
|
+
|
|
919
|
+
test('a queued control item keeps its proactive-review marker across projection', () => {
|
|
920
|
+
const session = {};
|
|
921
|
+
const marker = { id: 'review-1', workspace: 'docs', trigger: 'knowledge.ingested', sourceVersion: 'v1' };
|
|
922
|
+
dispatchAgentEvent(session, createAgentEvent('control_enqueued', {
|
|
923
|
+
origin: 'runtime',
|
|
924
|
+
workspace: 'docs',
|
|
925
|
+
payload: { id: 'control-1', workspace: 'docs', input: 'audit the workspace', proactiveReview: marker },
|
|
926
|
+
}));
|
|
927
|
+
|
|
928
|
+
assert.deepEqual(session.agentProjection.controlQueue[0].proactiveReview, marker);
|
|
929
|
+
});
|
|
930
|
+
|
|
931
|
+
test('streamed deltas are replaced by the final message, never duplicated', () => {
|
|
932
|
+
const session = {};
|
|
933
|
+
dispatchAgentEvent(session, createAgentEvent('assistant_delta', { origin: 'runtime', payload: { delta: 'Hello ' } }));
|
|
934
|
+
dispatchAgentEvent(session, createAgentEvent('assistant_delta', { origin: 'runtime', payload: { delta: 'world' } }));
|
|
935
|
+
assert.equal(session.agentProjection.conversation.at(-1).content, 'Hello world');
|
|
936
|
+
|
|
937
|
+
dispatchAgentEvent(session, createAgentEvent('assistant_message', { origin: 'runtime', payload: { content: 'Hello world' } }));
|
|
938
|
+
assert.equal(session.agentProjection.conversation.length, 1, 'the final message replaces the streamed one');
|
|
939
|
+
assert.equal(session.agentProjection.conversation[0].content, 'Hello world');
|
|
940
|
+
assert.ok(!session.agentProjection.conversation[0].streaming);
|
|
941
|
+
});
|
package/src/core/buildInfo.json
CHANGED
package/src/core/mcp.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { existsSync, readFileSync } from 'node:fs';
|
|
2
2
|
import { managerEnvFile, managerMcpEndpointsFile, readEnvFile } from './env.js';
|
|
3
3
|
|
|
4
|
-
const WIKI_MANAGER_VERSION = '0.15.
|
|
4
|
+
const WIKI_MANAGER_VERSION = '0.15.99';
|
|
5
5
|
|
|
6
6
|
function envValue(key) {
|
|
7
7
|
const filePath = managerEnvFile();
|