@dotdrelle/wiki-manager 0.15.62 → 0.15.66
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/README.md +1 -0
- package/package.json +1 -1
- package/src/agent/graph.js +31 -5
- package/src/agent/graph.test.js +1 -1
- package/src/agent/skillRecursion.test.js +77 -0
- package/src/cli/wiki-manager.js +2 -2
- package/src/commands/slash.js +11 -11
- package/src/core/activity.js +60 -10
- package/src/core/activity.test.js +59 -1
- package/src/core/agentLoop.js +3 -3
- package/src/core/agentLoop.test.js +1 -1
- package/src/core/buildInfo.json +2 -2
- package/src/core/mcp.js +1 -1
- package/src/core/skillChainView.js +14 -1
- package/src/core/skillChainView.test.js +13 -1
- package/src/core/skillInvocation.js +48 -0
- package/src/runtime/runner.js +6 -6
- package/src/runtime/runner.test.js +1 -1
- package/src/runtime/supervisor.js +3 -3
- package/src/shell/RightPane.tsx +33 -19
- package/src/shell/repl.js +14 -14
- package/src/shell/repl.test.js +5 -5
- package/src/shell/tui.tsx +6 -6
- package/src/shell/useAgent.ts +1 -1
- package/src/shell/useSession.ts +5 -5
package/README.md
CHANGED
|
@@ -401,6 +401,7 @@ answer "what is this and how do I start it", and stop there.
|
|
|
401
401
|
| [`docs/usage.md`](docs/usage.md) | The four ways to run wikiLLM, and how to configure the external agents |
|
|
402
402
|
| [`docs/configuration.md`](docs/configuration.md) | Every configuration key: root `.env`, Compose overrides, `mcp.endpoints.json`, workspace `.env`, `.wikirc.yaml`, parallelism |
|
|
403
403
|
| [`docs/technical-reference.md`](docs/technical-reference.md) | Workspace model, services, the `donna` shell, agent tooling, orchestration and activity contracts, security model |
|
|
404
|
+
| [`docs/authoring-skills.md`](docs/authoring-skills.md) | Writing a workspace skill: what splits a body into runs, chains, concurrency, parameters, and the interpretation rules |
|
|
404
405
|
| [`docs/claude-desktop.md`](docs/claude-desktop.md) | Using a workspace from Claude Desktop |
|
|
405
406
|
| [`CLAUDE.md`](CLAUDE.md) | Repository guidance: invariants to preserve when changing this code |
|
|
406
407
|
|
package/package.json
CHANGED
package/src/agent/graph.js
CHANGED
|
@@ -19,7 +19,7 @@ import {
|
|
|
19
19
|
truncateToolResult,
|
|
20
20
|
} from '../core/mcp.js';
|
|
21
21
|
import { findSkill, formatSkillsForAgent } from '../core/skills.js';
|
|
22
|
-
import { RESERVED_SLASH_COMMANDS, explicitSkillReference } from '../core/skillInvocation.js';
|
|
22
|
+
import { RESERVED_SLASH_COMMANDS, explicitSkillReference, objectiveNamesSkill } from '../core/skillInvocation.js';
|
|
23
23
|
import { handleSlashCommand } from '../commands/slash.js';
|
|
24
24
|
import { extractActivity, formatActivitySummary, parseJsonText, sessionActivities } from '../core/activity.js';
|
|
25
25
|
import { createAgentEvent, dispatchAgentEvent } from '../core/agentEvents.js';
|
|
@@ -947,6 +947,32 @@ export async function handleRuntimeControlTool(session, tool, args = {}) {
|
|
|
947
947
|
message: `Skill "${skillName}" is already running in this chain: execute its objective directly instead of re-invoking it.`,
|
|
948
948
|
});
|
|
949
949
|
}
|
|
950
|
+
/*
|
|
951
|
+
Depuis une intention compilée, une compétence se lance par son NOM, pas
|
|
952
|
+
par ressemblance.
|
|
953
|
+
|
|
954
|
+
La garde de cycle ci-dessus ne voit que les répétitions. Elle laissait
|
|
955
|
+
donc passer la cascade réellement observée sur un `/wiki-ingest` :
|
|
956
|
+
l'intention n°2 du corps est mot pour mot celui de
|
|
957
|
+
`/wiki-rebuild-concepts`, dont l'intention décrit à son tour
|
|
958
|
+
`/wiki-reclassify`, puis `/wiki-taxonomy`. Trois compétences distinctes,
|
|
959
|
+
aucun cycle, et la grille de concepts comme la taxonomie reconstruites
|
|
960
|
+
plusieurs fois pour une seule demande.
|
|
961
|
+
|
|
962
|
+
Une intention compilée EST déjà le travail à faire : elle se délègue.
|
|
963
|
+
La composition explicite reste ouverte — un corps qui nomme sa cible dit
|
|
964
|
+
ce qu'il veut ; une intention qui se contente de la décrire ne le dit
|
|
965
|
+
pas.
|
|
966
|
+
*/
|
|
967
|
+
if (skillStack.length > 0 && !objectiveNamesSkill(args._userInput, skillName)) {
|
|
968
|
+
return JSON.stringify({
|
|
969
|
+
ok: false,
|
|
970
|
+
terminal: true,
|
|
971
|
+
code: 'nested_skill_match_blocked',
|
|
972
|
+
skillStack,
|
|
973
|
+
message: `The current objective does not name skill "${skillName}"; it only describes what that skill does. Execute the objective yourself with runtime__delegate instead of re-routing it to another skill.`,
|
|
974
|
+
});
|
|
975
|
+
}
|
|
950
976
|
if (skillStack.length >= MAX_SKILL_DEPTH) {
|
|
951
977
|
return JSON.stringify({
|
|
952
978
|
ok: false,
|
|
@@ -1184,7 +1210,7 @@ export function buildAgentSystemPrompt(state) {
|
|
|
1184
1210
|
skills,
|
|
1185
1211
|
'</skill_catalog>',
|
|
1186
1212
|
runningSkillStack.length > 0
|
|
1187
|
-
? `You are already executing the compiled objective of workspace skill ${JSON.stringify(runningSkillStack.at(-1))}. The current user message IS that objective: execute it directly${runningSkillExecution === 'direct' ? ' and stop after its requested direct mutation; delegation and nested skills are forbidden for this workflow' : ' by delegating it with runtime__delegate (or a matching direct tool)'}. Do not select or call that skill again, with or without a leading slash — the runtime refuses the re-invocation with skill_recursion_blocked, and that refusal means act on the objective yourself, not report an error. A skill run is not successful until its requested mutation has an affirmative tool result; never infer success from the runtime merely becoming idle or done, and never end the run with an empty reply or a bare "{}".`
|
|
1213
|
+
? `You are already executing the compiled objective of workspace skill ${JSON.stringify(runningSkillStack.at(-1))}. The current user message IS that objective: execute it directly${runningSkillExecution === 'direct' ? ' and stop after its requested direct mutation; delegation and nested skills are forbidden for this workflow' : ' by delegating it with runtime__delegate (or a matching direct tool)'}. Do not select or call that skill again, with or without a leading slash — the runtime refuses the re-invocation with skill_recursion_blocked, and that refusal means act on the objective yourself, not report an error. Do not select ANY OTHER skill by description either: an objective necessarily reads like the description of the neighbouring skill that performs it, and re-routing it there re-runs work this objective already covers. From inside a compiled objective, runtime__run_skill is only for a skill the objective names explicitly; everything else is delegated with runtime__delegate. A skill run is not successful until its requested mutation has an affirmative tool result; never infer success from the runtime merely becoming idle or done, and never end the run with an empty reply or a bare "{}".`
|
|
1188
1214
|
: null,
|
|
1189
1215
|
'In interactive agent mode, call only tools actually provided to you. Any directly offered tool stays direct; never substitute an orchestration-contract tool yourself.',
|
|
1190
1216
|
'When the user asks for an action that can be performed with connected MCP tools or safe primitives, do not answer with future intent such as "I will call...", "I am going to run...", or "launching..." unless you also call the tool in the same turn. Either call the tool now, ask for the exact missing required arguments, or explain the concrete blocker.',
|
|
@@ -1269,8 +1295,8 @@ export function buildLimitedAgentResponse(state, reason = 'no workspace loaded w
|
|
|
1269
1295
|
}
|
|
1270
1296
|
|
|
1271
1297
|
export function formatLlmUnavailableMessage(reason) {
|
|
1272
|
-
const clean = String(reason ?? '
|
|
1273
|
-
return `⚠ LLM
|
|
1298
|
+
const clean = String(reason ?? 'unknown reason').replace(/\s+/g, ' ').trim();
|
|
1299
|
+
return `⚠ LLM unavailable: ${clean || 'unknown reason'}`;
|
|
1274
1300
|
}
|
|
1275
1301
|
|
|
1276
1302
|
function toolsForClassification(classification, writeTools, session = null) {
|
|
@@ -1429,7 +1455,7 @@ export function createAgentGraph(options = {}) {
|
|
|
1429
1455
|
const llm = state.session.llm ?? options.llm ?? null;
|
|
1430
1456
|
|
|
1431
1457
|
if (!llm) {
|
|
1432
|
-
return { response: formatLlmUnavailableMessage('
|
|
1458
|
+
return { response: formatLlmUnavailableMessage('no LLM client configured'), pendingToolCalls: null, readyToStream: false };
|
|
1433
1459
|
}
|
|
1434
1460
|
|
|
1435
1461
|
const iterations = state.toolIterations ?? 0;
|
package/src/agent/graph.test.js
CHANGED
|
@@ -292,7 +292,7 @@ test('agent graph reports LLM unavailable without Donna active boilerplate', asy
|
|
|
292
292
|
const agent = createAgentGraph();
|
|
293
293
|
const result = await agent.invoke({ input: 'salut', session: sessionBase({ llm: null }) });
|
|
294
294
|
|
|
295
|
-
assert.equal(result.response, '⚠ LLM
|
|
295
|
+
assert.equal(result.response, '⚠ LLM unavailable: no LLM client configured');
|
|
296
296
|
assert.doesNotMatch(result.response, /Donna is active/);
|
|
297
297
|
});
|
|
298
298
|
|
|
@@ -96,3 +96,80 @@ test('borne la profondeur même sans cycle', async () => {
|
|
|
96
96
|
|
|
97
97
|
assert.equal(result.code, 'skill_depth_exceeded');
|
|
98
98
|
});
|
|
99
|
+
|
|
100
|
+
/*
|
|
101
|
+
Cascade observée sur un `/wiki-ingest` : deux `/wiki-rebuild-concepts`, puis
|
|
102
|
+
`/wiki-reclassify`, puis `/wiki-taxonomy`. Aucun cycle — trois compétences
|
|
103
|
+
distinctes — mais la grille de concepts et la taxonomie reconstruites
|
|
104
|
+
plusieurs fois pour une seule demande. La deuxième intention compilée de
|
|
105
|
+
`/wiki-ingest` est mot pour mot le corps de `/wiki-rebuild-concepts` : le
|
|
106
|
+
sélecteur par description la reconnaissait légitimement.
|
|
107
|
+
*/
|
|
108
|
+
const callWithObjective = (state, skillName, objective) =>
|
|
109
|
+
handleRuntimeControlTool(state, 'run_skill', { skillName, _userInput: objective })
|
|
110
|
+
.then((raw) => JSON.parse(raw));
|
|
111
|
+
|
|
112
|
+
test('refuse une compétence voisine que l’intention décrit sans la nommer', async () => {
|
|
113
|
+
const objective = 'Run the production pipeline steps concepts, reclassify-concepts and taxonomy, in that order.';
|
|
114
|
+
const ran = [];
|
|
115
|
+
const result = await callWithObjective(session(['wiki-ingest'], ran), 'wiki-rebuild-concepts', objective);
|
|
116
|
+
|
|
117
|
+
assert.equal(result.ok, false);
|
|
118
|
+
assert.equal(result.code, 'nested_skill_match_blocked');
|
|
119
|
+
assert.deepEqual(ran, [], 'un refus ne doit lancer aucun travail');
|
|
120
|
+
assert.match(result.message, /runtime__delegate/);
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
test('laisse passer la compétence que l’intention nomme explicitement', async () => {
|
|
124
|
+
const ran = [];
|
|
125
|
+
const result = await callWithObjective(session(['wiki-sync'], ran), 'deliver', 'Then run /deliver on the produced report.');
|
|
126
|
+
|
|
127
|
+
assert.equal(result.ok, true);
|
|
128
|
+
assert.deepEqual(ran, ['deliver']);
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
test('ne confond pas un nom de compétence avec son préfixe', async () => {
|
|
132
|
+
const result = await callWithObjective(session(['pipeline']), 'wiki-build', 'Hand the result to the wiki-builder service.');
|
|
133
|
+
|
|
134
|
+
assert.equal(result.code, 'nested_skill_match_blocked');
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
test('un chemin de fichier commençant par un nom de compétence ne vaut pas invocation', async () => {
|
|
138
|
+
const result = await callWithObjective(
|
|
139
|
+
session(['wiki-ingest']),
|
|
140
|
+
'wiki-build',
|
|
141
|
+
'Move the leaf file /wiki-build/unclassified/x.md into place then continue.',
|
|
142
|
+
);
|
|
143
|
+
|
|
144
|
+
assert.equal(result.code, 'nested_skill_match_blocked');
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
test('hors de toute chaîne, la sélection par description reste permise', async () => {
|
|
148
|
+
const result = await callWithObjective(session(undefined), 'wiki-taxonomy', 'republish the graph taxonomy');
|
|
149
|
+
|
|
150
|
+
assert.equal(result.ok, true);
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
/*
|
|
154
|
+
Le nom seul ne prouve rien. Plusieurs compétences du scaffold portent un nom
|
|
155
|
+
qui est aussi un mot courant : « Run the production pipeline steps concepts,
|
|
156
|
+
reclassify-concepts and taxonomy » nomme `pipeline`, dont le lancement rejoue
|
|
157
|
+
ingest + build + export + polish. Une intention doit citer sa cible EN TANT QUE
|
|
158
|
+
compétence, pas l'employer comme mot.
|
|
159
|
+
*/
|
|
160
|
+
test('un nom employé comme mot courant ne vaut pas invocation', async () => {
|
|
161
|
+
const objective = 'Run the production pipeline steps concepts, reclassify-concepts and taxonomy, in that order.';
|
|
162
|
+
const ran = [];
|
|
163
|
+
const result = await callWithObjective(session(['wiki-ingest'], ran), 'pipeline', objective);
|
|
164
|
+
|
|
165
|
+
assert.equal(result.code, 'nested_skill_match_blocked');
|
|
166
|
+
assert.deepEqual(ran, []);
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
test('la tournure « the deliver skill » vaut invocation explicite', async () => {
|
|
170
|
+
const ran = [];
|
|
171
|
+
const result = await callWithObjective(session(['wiki-sync'], ran), 'deliver', 'Hand the report to the deliver skill.');
|
|
172
|
+
|
|
173
|
+
assert.equal(result.ok, true);
|
|
174
|
+
assert.deepEqual(ran, ['deliver']);
|
|
175
|
+
});
|
package/src/cli/wiki-manager.js
CHANGED
|
@@ -24,7 +24,7 @@ import { applySessionWikircProfile } from '../core/sessionConfig.js';
|
|
|
24
24
|
import { listWikircProfiles } from '../core/wikirc.js';
|
|
25
25
|
import { callMcpTool, formatMcpToolResult, readChatAccessConfig } from '../core/mcp.js';
|
|
26
26
|
import { deleteManagedMcpEndpoint, listManagedMcpEndpoints, upsertManagedMcpEndpoint } from '../core/mcpEndpoints.js';
|
|
27
|
-
import { extractActivity, parseJsonText, sessionActivities, terminalFailures } from '../core/activity.js';
|
|
27
|
+
import { extractActivity, mergePolledActivity, parseJsonText, sessionActivities, terminalFailures } from '../core/activity.js';
|
|
28
28
|
import { syncActivitiesToPlan, formatPlanStatus } from '../core/plan.js';
|
|
29
29
|
import { createAgentEvent, dispatchAgentEvent, reduceAgentEvents } from '../core/agentEvents.js';
|
|
30
30
|
import { runAgentTurn, runAgenticLoop } from '../core/agentLoop.js';
|
|
@@ -432,7 +432,7 @@ async function runHeadlessActivityLoop(session, log, { wait, timeoutMs }) {
|
|
|
432
432
|
try {
|
|
433
433
|
const result = await callMcpTool(session.mcp, activity.poll.server, activity.poll.tool, activity.poll.args ?? {});
|
|
434
434
|
const payload = parseJsonText(formatMcpToolResult(result));
|
|
435
|
-
const polledActivity = extractActivity(payload, { server: activity.poll.server, tool: activity.poll.tool });
|
|
435
|
+
const polledActivity = mergePolledActivity(activity, extractActivity(payload, { server: activity.poll.server, tool: activity.poll.tool }));
|
|
436
436
|
if (polledActivity) {
|
|
437
437
|
dispatchAgentEvent(session, createAgentEvent('activity_upserted', {
|
|
438
438
|
origin: 'poll',
|
package/src/commands/slash.js
CHANGED
|
@@ -1386,7 +1386,7 @@ export async function handleSlashCommand(line, context) {
|
|
|
1386
1386
|
if (!context.session.workspace) return { output: 'No workspace loaded. Use /use <workspace> first.' };
|
|
1387
1387
|
const operation = args[3] && !args[3].includes('.') && !args[3].includes('/') ? args[3] : undefined;
|
|
1388
1388
|
const inputs = args.slice(operation ? 4 : 3);
|
|
1389
|
-
const result = await postRuntimeRun(`
|
|
1389
|
+
const result = await postRuntimeRun(`Capability run ${capability}${operation ? ` (${operation})` : ''} requested via /run capability.`, {
|
|
1390
1390
|
url,
|
|
1391
1391
|
workspace: context.session.workspace,
|
|
1392
1392
|
capabilityPlan: {
|
|
@@ -1396,9 +1396,9 @@ export async function handleSlashCommand(line, context) {
|
|
|
1396
1396
|
},
|
|
1397
1397
|
});
|
|
1398
1398
|
if (result?.runId) {
|
|
1399
|
-
return { output: `▶
|
|
1399
|
+
return { output: `▶ Capability run accepted (${String(result.runId).slice(0, 8)}) — the agent's plan will be integrated and dispatched in parallel; approval requested before mutations (/approve).` };
|
|
1400
1400
|
}
|
|
1401
|
-
return { output: `Run
|
|
1401
|
+
return { output: `Run not started: ${result?.explanation ?? result?.error ?? JSON.stringify(result)}` };
|
|
1402
1402
|
}
|
|
1403
1403
|
if (subcommand === 'kill') {
|
|
1404
1404
|
const result = await postRuntimeKill({ url, workspace: context.session.workspace ?? null, runId: args[2] ?? null });
|
|
@@ -1419,7 +1419,7 @@ export async function handleSlashCommand(line, context) {
|
|
|
1419
1419
|
const runActive = String(context.session.agentProjection?.status ?? '').toLowerCase() === 'running';
|
|
1420
1420
|
if (count === 0 && (activeRuntimeItems > 0 || runActive)) {
|
|
1421
1421
|
return {
|
|
1422
|
-
output: `Cleared 0 finished queue items — ${activeRuntimeItems || '
|
|
1422
|
+
output: `Cleared 0 finished queue items — ${activeRuntimeItems || 'the'} active item(s) are managed by the runtime${runActive ? ' (run in progress)' : ''}. Use /run cancel (graceful stop) or /run kill (abort + full purge).`,
|
|
1423
1423
|
};
|
|
1424
1424
|
}
|
|
1425
1425
|
return { output: `Cleared ${count} finished queue item${count === 1 ? '' : 's'}.` };
|
|
@@ -1433,7 +1433,7 @@ export async function handleSlashCommand(line, context) {
|
|
|
1433
1433
|
// would silently revert the item to waiting (fake cancel).
|
|
1434
1434
|
const localItem = (context.session.jobQueue ?? []).find((item) => String(item.id) === String(id));
|
|
1435
1435
|
if (localItem?.origin === 'runtime' || (!localItem && runtimeManagedItemId(context, id))) {
|
|
1436
|
-
if (!context.runtime?.url) return { output: '
|
|
1436
|
+
if (!context.runtime?.url) return { output: 'Runtime-managed item — reconnect the runtime to cancel it, or use /run cancel or /run kill.' };
|
|
1437
1437
|
try {
|
|
1438
1438
|
const result = await postRuntimeControl('cancel_item', {
|
|
1439
1439
|
url: context.runtime.url,
|
|
@@ -1659,13 +1659,13 @@ export async function handleSlashCommand(line, context) {
|
|
|
1659
1659
|
try {
|
|
1660
1660
|
const killed = await postRuntimeKill({ url: runtime.url, workspace, runId: null, purge: true });
|
|
1661
1661
|
const purged = killed.purged ?? { runs: 0, events: 0, queue: 0 };
|
|
1662
|
-
parts.push(`runtime
|
|
1663
|
-
parts.push(`store
|
|
1662
|
+
parts.push(`runtime: ${killed.runs ?? 0} run(s) stopped, ${killed.tasks ?? 0} task(s), ${killed.queued ?? 0} request(s)`);
|
|
1663
|
+
parts.push(`store purged: ${purged.runs ?? 0} run(s), ${purged.events ?? 0} event(s), ${purged.queue ?? 0} queue item(s)`);
|
|
1664
1664
|
} catch (err) {
|
|
1665
|
-
parts.push(`runtime kill
|
|
1665
|
+
parts.push(`runtime kill failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
1666
1666
|
}
|
|
1667
1667
|
} else {
|
|
1668
|
-
parts.push('runtime
|
|
1668
|
+
parts.push('runtime not connected (nothing to purge server-side)');
|
|
1669
1669
|
}
|
|
1670
1670
|
|
|
1671
1671
|
const clearedQueue = clearFinishedQueueItems(context.session);
|
|
@@ -1686,9 +1686,9 @@ export async function handleSlashCommand(line, context) {
|
|
|
1686
1686
|
context.session.workflow = null;
|
|
1687
1687
|
context.session.jobQueue = [];
|
|
1688
1688
|
context.session.productionActivity = null;
|
|
1689
|
-
parts.push(`
|
|
1689
|
+
parts.push(`local queue: ${clearedQueue} finished item(s) cleared`);
|
|
1690
1690
|
|
|
1691
|
-
return { output: `Interface
|
|
1691
|
+
return { output: `Interface reset (--all) — ${parts.join(' · ')}.` };
|
|
1692
1692
|
}
|
|
1693
1693
|
case 'exit':
|
|
1694
1694
|
case 'quit':
|
package/src/core/activity.js
CHANGED
|
@@ -104,7 +104,19 @@ export function normalizeActivity(activity, fallback = {}) {
|
|
|
104
104
|
return normalized;
|
|
105
105
|
}
|
|
106
106
|
|
|
107
|
-
|
|
107
|
+
// A status payload can only be polled with the tool that produced it. Any
|
|
108
|
+
// `*_status` tool qualifies; anything else (a `*_start_job` call whose result
|
|
109
|
+
// carried the first snapshot) falls back to the production status tool, and
|
|
110
|
+
// for a non-production agent to null — the caller then keeps the poll
|
|
111
|
+
// descriptor the tracked activity already had.
|
|
112
|
+
function statusPollTool(context, source) {
|
|
113
|
+
const tool = String(context?.tool ?? '');
|
|
114
|
+
if (/status$/i.test(tool)) return tool;
|
|
115
|
+
return source === 'production' ? 'production_job_status' : null;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function activityFromStatusPayload(payload, context = {}) {
|
|
119
|
+
const source = String(context?.server ?? 'production');
|
|
108
120
|
const progress = payload?.progress;
|
|
109
121
|
const job = payload?.job;
|
|
110
122
|
const jobId = payload?.jobId ?? job?.jobId;
|
|
@@ -141,11 +153,12 @@ function productionActivityFromPayload(payload, context = {}) {
|
|
|
141
153
|
progressDetail,
|
|
142
154
|
progress?.lastEvent ? `last ${progress.lastEvent}` : null,
|
|
143
155
|
].filter(Boolean).join(' · ');
|
|
156
|
+
const prefix = source === 'production' ? 'Production' : source;
|
|
144
157
|
return normalizeActivity({
|
|
145
158
|
id: jobId,
|
|
146
|
-
source
|
|
159
|
+
source,
|
|
147
160
|
kind: job?.type ?? payload?.operation ?? payload?.type ?? progress?.phase ?? progress?.currentStep ?? 'job',
|
|
148
|
-
label: detail ?
|
|
161
|
+
label: detail ? `${prefix}: ${detail}` : `${prefix}: ${status}`,
|
|
149
162
|
status,
|
|
150
163
|
progress: {
|
|
151
164
|
...(progress ?? {}),
|
|
@@ -154,9 +167,9 @@ function productionActivityFromPayload(payload, context = {}) {
|
|
|
154
167
|
...(payload?.taskId ? { stepId: String(payload.taskId) } : {}),
|
|
155
168
|
},
|
|
156
169
|
plan: Array.isArray(progress?.steps) ? { steps: progress.steps } : null,
|
|
157
|
-
poll: jobId ? {
|
|
158
|
-
server:
|
|
159
|
-
tool: context
|
|
170
|
+
poll: jobId && statusPollTool(context, source) ? {
|
|
171
|
+
server: source,
|
|
172
|
+
tool: statusPollTool(context, source),
|
|
160
173
|
args: { jobId },
|
|
161
174
|
intervalMs: 2500,
|
|
162
175
|
} : null,
|
|
@@ -170,10 +183,47 @@ export function extractActivity(payload, context = {}) {
|
|
|
170
183
|
if (payload._activity) {
|
|
171
184
|
return normalizeActivity(payload._activity, { source: context.server });
|
|
172
185
|
}
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
186
|
+
// Without an explicit `_activity` opt-in, a status payload is inferred into an
|
|
187
|
+
// activity only where we already know how it would be polled: the production
|
|
188
|
+
// server (whose `*_start_job` result carries the first snapshot), or any agent
|
|
189
|
+
// answering through a `*_status` tool. This is what lets a job run by another
|
|
190
|
+
// executor (knowledge.update on agent-cme, polled via `agent_status`) update
|
|
191
|
+
// its percentage instead of the panel showing the dispatcher's initial 0 %
|
|
192
|
+
// for the whole run. An arbitrary agent-mode tool call that merely happens to
|
|
193
|
+
// return a `jobId`/`progress`-shaped object must NOT create or replace the run
|
|
194
|
+
// plan — monitoring stays opt-in (`_activity`) for it.
|
|
195
|
+
const source = String(context?.server ?? 'production');
|
|
196
|
+
if (source !== 'production' && !statusPollTool(context, source)) return null;
|
|
197
|
+
return activityFromStatusPayload(payload, context);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// A poll answers with the AGENT's view of the job: it knows the job id, the
|
|
201
|
+
// status and the progress, but nothing about the plan task the orchestrator
|
|
202
|
+
// dispatched it for. Re-normalizing that answer on its own therefore dropped
|
|
203
|
+
// `progress.stepId` and could re-key the activity under a different source,
|
|
204
|
+
// breaking the task<->activity link the Activity/Plan panels use to attach a
|
|
205
|
+
// live percentage to a plan step. Merge onto the tracked activity instead:
|
|
206
|
+
// identity and linkage come from what we already know, live values from the
|
|
207
|
+
// poll.
|
|
208
|
+
export function mergePolledActivity(tracked, polled) {
|
|
209
|
+
if (!polled) return null;
|
|
210
|
+
if (!tracked) return polled;
|
|
211
|
+
const stepId = polled.progress?.stepId ?? tracked.progress?.stepId ?? null;
|
|
212
|
+
return normalizeActivity({
|
|
213
|
+
...polled,
|
|
214
|
+
// Identity: keep the tracked source/id so activityKey() stays stable.
|
|
215
|
+
id: tracked.id ?? polled.id,
|
|
216
|
+
source: tracked.source ?? polled.source,
|
|
217
|
+
// Keep polling with the descriptor that worked when the agent's answer
|
|
218
|
+
// does not carry one of its own.
|
|
219
|
+
poll: polled.poll ?? tracked.poll,
|
|
220
|
+
plan: polled.plan ?? tracked.plan ?? null,
|
|
221
|
+
startedAt: tracked.startedAt ?? polled.startedAt ?? null,
|
|
222
|
+
progress: {
|
|
223
|
+
...(polled.progress ?? {}),
|
|
224
|
+
...(stepId !== null ? { stepId } : {}),
|
|
225
|
+
},
|
|
226
|
+
});
|
|
177
227
|
}
|
|
178
228
|
|
|
179
229
|
export function rememberActivity(session, activity) {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { test } from 'node:test';
|
|
2
2
|
import assert from 'node:assert/strict';
|
|
3
|
-
import { normalizeActivity, extractActivity, isCancelledStatus, rememberActivity, rememberActivityFromPayload } from './activity.js';
|
|
3
|
+
import { normalizeActivity, extractActivity, mergePolledActivity, isCancelledStatus, rememberActivity, rememberActivityFromPayload } from './activity.js';
|
|
4
4
|
|
|
5
5
|
test('normalizeActivity: plan.steps preserved with id and label', () => {
|
|
6
6
|
const a = normalizeActivity({
|
|
@@ -171,3 +171,61 @@ test('rememberActivityFromPayload: returns null for irrelevant payload', () => {
|
|
|
171
171
|
const session = {};
|
|
172
172
|
assert.equal(rememberActivityFromPayload(session, { message: 'ok' }), null);
|
|
173
173
|
});
|
|
174
|
+
|
|
175
|
+
test('extractActivity: a non-production agent status payload yields an activity', () => {
|
|
176
|
+
const activity = extractActivity({
|
|
177
|
+
jobId: 'job-know-1',
|
|
178
|
+
status: 'running',
|
|
179
|
+
progress: { percent: 42, phase: 'knowledge.update', detail: 'chunk 3/7' },
|
|
180
|
+
}, { server: 'cme', tool: 'agent_status' });
|
|
181
|
+
assert.ok(activity, 'a knowledge.update job run outside the production server must still produce an activity');
|
|
182
|
+
assert.equal(activity.source, 'cme');
|
|
183
|
+
assert.equal(activity.progress.percent, 42);
|
|
184
|
+
assert.equal(activity.poll.server, 'cme');
|
|
185
|
+
assert.equal(activity.poll.tool, 'agent_status');
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
test('extractActivity: a non-status tool result on another server is not inferred into an activity', () => {
|
|
189
|
+
// Agent mode: the LLM calls an arbitrary connector tool whose result happens
|
|
190
|
+
// to carry a jobId/progress-shaped object. Without an `_activity` opt-in and
|
|
191
|
+
// without a `*_status` poll tool, this must not create or replace the plan.
|
|
192
|
+
const activity = extractActivity({
|
|
193
|
+
jobId: 'export-42',
|
|
194
|
+
progress: { steps: [{ id: 'a', label: 'A' }, { id: 'b', label: 'B' }] },
|
|
195
|
+
}, { server: 'connectors', tool: 'gmail_export_run' });
|
|
196
|
+
assert.equal(activity, null);
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
test('extractActivity: production start-job result is still inferred without a status tool', () => {
|
|
200
|
+
const activity = extractActivity({
|
|
201
|
+
jobId: 'job-1',
|
|
202
|
+
status: 'running',
|
|
203
|
+
progress: { percent: 5, phase: 'ingest' },
|
|
204
|
+
}, { server: 'production', tool: 'production_start_job' });
|
|
205
|
+
assert.ok(activity);
|
|
206
|
+
assert.equal(activity.source, 'production');
|
|
207
|
+
assert.equal(activity.poll.tool, 'production_job_status');
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
test('mergePolledActivity: keeps the tracked key, poll and stepId across polls', () => {
|
|
211
|
+
const tracked = normalizeActivity({
|
|
212
|
+
id: 'job-know-1',
|
|
213
|
+
source: 'cme',
|
|
214
|
+
kind: 'knowledge.update',
|
|
215
|
+
label: 'Mise a jour des connaissances',
|
|
216
|
+
status: 'queued',
|
|
217
|
+
progress: { percent: 0, stepId: 'task-3' },
|
|
218
|
+
poll: { server: 'cme', tool: 'agent_status', args: { jobId: 'job-know-1' }, intervalMs: 1000 },
|
|
219
|
+
});
|
|
220
|
+
const polled = extractActivity({
|
|
221
|
+
jobId: 'job-know-1',
|
|
222
|
+
status: 'running',
|
|
223
|
+
progress: { percent: 63, detail: 'chunk 5/7' },
|
|
224
|
+
}, { server: 'cme', tool: 'agent_status' });
|
|
225
|
+
const merged = mergePolledActivity(tracked, polled);
|
|
226
|
+
assert.equal(merged.key, tracked.key, 'the activity must not be re-keyed by a poll');
|
|
227
|
+
assert.equal(merged.progress.stepId, 'task-3', 'the plan task link must survive a poll');
|
|
228
|
+
assert.equal(merged.progress.percent, 63);
|
|
229
|
+
assert.equal(merged.poll.tool, 'agent_status');
|
|
230
|
+
assert.equal(merged.terminal, false);
|
|
231
|
+
});
|
package/src/core/agentLoop.js
CHANGED
|
@@ -36,7 +36,7 @@ export async function runAgentTurn(agent, session, input, {
|
|
|
36
36
|
if (session._abortSignal === signal) delete session._abortSignal;
|
|
37
37
|
}
|
|
38
38
|
if (result.streamedInline) {
|
|
39
|
-
return streamedContent.trim() || formatLlmUnavailableMessage('
|
|
39
|
+
return streamedContent.trim() || formatLlmUnavailableMessage('empty stream');
|
|
40
40
|
}
|
|
41
41
|
if (result.response != null) return result.response;
|
|
42
42
|
if (result.readyToStream && session.llm?.stream) {
|
|
@@ -49,9 +49,9 @@ export async function runAgentTurn(agent, session, input, {
|
|
|
49
49
|
})) {
|
|
50
50
|
content += delta;
|
|
51
51
|
}
|
|
52
|
-
return content.trim() || formatLlmUnavailableMessage('
|
|
52
|
+
return content.trim() || formatLlmUnavailableMessage('empty stream');
|
|
53
53
|
}
|
|
54
|
-
return formatLlmUnavailableMessage('
|
|
54
|
+
return formatLlmUnavailableMessage('empty response');
|
|
55
55
|
}
|
|
56
56
|
|
|
57
57
|
export async function runAgenticLoop(agent, session, initialInput, {
|
|
@@ -18,7 +18,7 @@ test('runAgentTurn returns a one-line LLM error on empty stream', async () => {
|
|
|
18
18
|
|
|
19
19
|
const response = await runAgentTurn(agent, session, 'salut');
|
|
20
20
|
|
|
21
|
-
assert.equal(response, '⚠ LLM
|
|
21
|
+
assert.equal(response, '⚠ LLM unavailable: empty stream');
|
|
22
22
|
});
|
|
23
23
|
|
|
24
24
|
test('runAgenticLoop waits for new activities and continues with a completion summary', async () => {
|
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.66';
|
|
5
5
|
|
|
6
6
|
function envValue(key) {
|
|
7
7
|
const filePath = managerEnvFile();
|
|
@@ -16,6 +16,18 @@ const SYMBOLS = {
|
|
|
16
16
|
skipped: '–',
|
|
17
17
|
};
|
|
18
18
|
|
|
19
|
+
// The selection reason is an audit enum (`explicit_name` / `description_match`);
|
|
20
|
+
// leaking it verbatim into a queue label read as a broken token (`[explicit_name]`).
|
|
21
|
+
// Humanize it for display; keep the raw value on `selectionKind` for audit.
|
|
22
|
+
const SELECTION_KIND_LABELS = {
|
|
23
|
+
explicit_name: 'explicit name',
|
|
24
|
+
description_match: 'description match',
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
export function selectionKindLabel(selectionKind) {
|
|
28
|
+
return SELECTION_KIND_LABELS[selectionKind] ?? selectionKind ?? null;
|
|
29
|
+
}
|
|
30
|
+
|
|
19
31
|
export const TERMINAL = new Set(['done', 'failed', 'cancelled', 'skipped']);
|
|
20
32
|
|
|
21
33
|
// Objectives are whole paragraphs; a chain view needs a line. Keep the first
|
|
@@ -61,6 +73,7 @@ export function projectSkillChains(controlQueue = []) {
|
|
|
61
73
|
chainId,
|
|
62
74
|
skillName: chainItems.find((item) => item.skillName)?.skillName ?? null,
|
|
63
75
|
selectionKind: chainItems.find((item) => item.selectionKind)?.selectionKind ?? null,
|
|
76
|
+
selectionLabel: selectionKindLabel(chainItems.find((item) => item.selectionKind)?.selectionKind ?? null),
|
|
64
77
|
steps,
|
|
65
78
|
status: chainStatus(steps),
|
|
66
79
|
};
|
|
@@ -79,7 +92,7 @@ function chainStatus(steps) {
|
|
|
79
92
|
// The text form used by the Shell; serve renders the same projection as DOM.
|
|
80
93
|
export function renderSkillChain(chain) {
|
|
81
94
|
if (!chain?.steps?.length) return '';
|
|
82
|
-
const selection = chain.
|
|
95
|
+
const selection = chain.selectionLabel ? ` · ${chain.selectionLabel}` : '';
|
|
83
96
|
const lines = [`${chain.skillName ?? 'skill'}${selection}`, ''];
|
|
84
97
|
for (const step of chain.steps) {
|
|
85
98
|
lines.push(`${step.symbol} ${step.label}`);
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import test from 'node:test';
|
|
2
2
|
import assert from 'node:assert/strict';
|
|
3
|
-
import { chainStepLabel, projectSkillChains, renderSkillChain } from './skillChainView.js';
|
|
3
|
+
import { chainStepLabel, projectSkillChains, renderSkillChain, selectionKindLabel } from './skillChainView.js';
|
|
4
4
|
|
|
5
5
|
const WIKI_SYNC = [
|
|
6
6
|
{
|
|
@@ -48,3 +48,15 @@ test('standalone control items are not chains', () => {
|
|
|
48
48
|
assert.deepEqual(projectSkillChains([{ id: 'x', status: 'queued', input: 'do something' }]), []);
|
|
49
49
|
assert.deepEqual(projectSkillChains(), []);
|
|
50
50
|
});
|
|
51
|
+
|
|
52
|
+
test('the selection reason is humanized, not leaked as an audit enum', () => {
|
|
53
|
+
assert.equal(selectionKindLabel('explicit_name'), 'explicit name');
|
|
54
|
+
assert.equal(selectionKindLabel('description_match'), 'description match');
|
|
55
|
+
assert.equal(selectionKindLabel(null), null);
|
|
56
|
+
const [chain] = projectSkillChains([
|
|
57
|
+
{ id: 'c0', chainId: 'k', chainSequence: 0, skillName: 'wiki-taxonomy', selectionKind: 'explicit_name', status: 'running', input: '/wiki-taxonomy' },
|
|
58
|
+
]);
|
|
59
|
+
assert.equal(chain.selectionKind, 'explicit_name');
|
|
60
|
+
assert.equal(chain.selectionLabel, 'explicit name');
|
|
61
|
+
assert.equal(renderSkillChain(chain).split('\n')[0], 'wiki-taxonomy · explicit name');
|
|
62
|
+
});
|
|
@@ -19,6 +19,54 @@ export function explicitSkillReference(input, skillName, language = null) {
|
|
|
19
19
|
return text.some((sentence) => patterns.some((pattern) => pattern.test(sentence)));
|
|
20
20
|
}
|
|
21
21
|
|
|
22
|
+
/*
|
|
23
|
+
Une intention compilée NOMME-t-elle la compétence qu'on veut lancer depuis
|
|
24
|
+
elle ?
|
|
25
|
+
|
|
26
|
+
Le corps d'une compétence est compilé en intentions métier, et une intention
|
|
27
|
+
décrit forcément ce que fait une compétence voisine : la deuxième intention de
|
|
28
|
+
`/wiki-ingest` est mot pour mot le corps de `/wiki-rebuild-concepts`. Le
|
|
29
|
+
sélecteur par description la reconnaissait donc et relançait la compétence
|
|
30
|
+
voisine, qui relançait la suivante — concepts et taxonomie produits plusieurs
|
|
31
|
+
fois pour un seul `/wiki-ingest`.
|
|
32
|
+
|
|
33
|
+
La composition volontaire reste possible : un corps qui écrit `/deliver` ou
|
|
34
|
+
« the deliver skill » nomme sa cible, et se distingue ainsi d'une intention
|
|
35
|
+
qui se contente de la décrire. C'est le seul signal qui ne dépende pas de ce
|
|
36
|
+
que le modèle déclare de sa propre sélection.
|
|
37
|
+
*/
|
|
38
|
+
export function objectiveNamesSkill(input, skillName) {
|
|
39
|
+
const raw = String(skillName ?? '').trim();
|
|
40
|
+
const name = escapeRegExp(raw);
|
|
41
|
+
if (!name) return false;
|
|
42
|
+
const text = String(input ?? '').trim();
|
|
43
|
+
// Une invocation directe : la demande EST le nom, rien d'autre.
|
|
44
|
+
if (text.toLowerCase() === raw.toLowerCase()) return true;
|
|
45
|
+
/*
|
|
46
|
+
Le nom seul ne suffit pas : plusieurs compétences du scaffold portent un nom
|
|
47
|
+
qui est aussi un mot courant. « Run the production pipeline steps concepts,
|
|
48
|
+
reclassify-concepts and taxonomy » nomme ainsi la compétence `pipeline`, qui
|
|
49
|
+
relance ingest + build + export + polish — bien pire que la cascade qu'on
|
|
50
|
+
corrige. Le nom doit donc être cité EN TANT QUE compétence : forme slash, ou
|
|
51
|
+
tournure explicite. La borne droite est écrite à la main, `\b` ne bornant pas
|
|
52
|
+
après un `-` final (`wiki-build` ne doit pas matcher dans `wiki-builder`).
|
|
53
|
+
*/
|
|
54
|
+
const end = '(?![A-Za-z0-9_-])';
|
|
55
|
+
// The slash form is a command, not a path: `/wiki-build` followed by `/` is
|
|
56
|
+
// `wiki/concepts/...`-style text referencing a file, not an invocation of the
|
|
57
|
+
// `wiki-build` skill. A trailing `/` must not satisfy the right boundary here,
|
|
58
|
+
// or a compiled objective that merely names a path re-opens the nested-skill
|
|
59
|
+
// cascade this guard exists to close.
|
|
60
|
+
const slashEnd = '(?![A-Za-z0-9_/-])';
|
|
61
|
+
const keyword = '(?:skill|workflow|compétence)';
|
|
62
|
+
return [
|
|
63
|
+
new RegExp(`(?:^|[^A-Za-z0-9_-])/${name}${slashEnd}`, 'i'),
|
|
64
|
+
new RegExp(`\\b${keyword}\\s+/?${name}${end}`, 'i'),
|
|
65
|
+
new RegExp(`(?:^|[^A-Za-z0-9_-])/?${name}${end}\\s+${keyword}\\b`, 'i'),
|
|
66
|
+
new RegExp(`/skills\\s+run\\s+${name}${end}`, 'i'),
|
|
67
|
+
].some((pattern) => pattern.test(text));
|
|
68
|
+
}
|
|
69
|
+
|
|
22
70
|
export function matchSkillInvocation(session, input, { allowReserved = false } = {}) {
|
|
23
71
|
const match = INVOCATION_RE.exec(String(input ?? '').trim());
|
|
24
72
|
if (!match) return null;
|
package/src/runtime/runner.js
CHANGED
|
@@ -116,7 +116,7 @@ export async function runRuntimeAgenticLoop(agent, session, initialInput, { sign
|
|
|
116
116
|
dispatchAgentEvent(session, createAgentEvent('assistant_message', {
|
|
117
117
|
origin: 'runtime',
|
|
118
118
|
runId,
|
|
119
|
-
payload: { content: summary || 'Action
|
|
119
|
+
payload: { content: summary || 'Action completed.' },
|
|
120
120
|
}));
|
|
121
121
|
},
|
|
122
122
|
onMaxTurns: ({ maxTurns: totalTurns }) => {
|
|
@@ -261,7 +261,7 @@ export async function runRuntimeAgenticWorkflow(agent, session, input, {
|
|
|
261
261
|
origin: 'runtime',
|
|
262
262
|
runId,
|
|
263
263
|
payload: {
|
|
264
|
-
content: `
|
|
264
|
+
content: `The run finished but the evaluation judged it incomplete: ${evaluation.reason}`,
|
|
265
265
|
},
|
|
266
266
|
}));
|
|
267
267
|
dispatchAgentEvent(session, createAgentEvent('run_error', {
|
|
@@ -570,10 +570,10 @@ export async function runRuntimeParallelPlan(agent, session, input, {
|
|
|
570
570
|
runId,
|
|
571
571
|
payload: {
|
|
572
572
|
content: [
|
|
573
|
-
`⏸
|
|
573
|
+
`⏸ Approval required before execution: ${newlyRequested.length} mutating task(s) pending.`,
|
|
574
574
|
...newlyRequested.slice(0, 5).map((step) => ` - ${step.description ?? step.id}`),
|
|
575
|
-
newlyRequested.length > 5 ? ` …
|
|
576
|
-
'
|
|
575
|
+
newlyRequested.length > 5 ? ` … and ${newlyRequested.length - 5} more.` : null,
|
|
576
|
+
'Type /approve (or click "Approve") to start, "cancel" to abandon.',
|
|
577
577
|
].filter(Boolean).join('\n'),
|
|
578
578
|
},
|
|
579
579
|
}));
|
|
@@ -591,7 +591,7 @@ export async function runRuntimeParallelPlan(agent, session, input, {
|
|
|
591
591
|
origin: 'runtime',
|
|
592
592
|
runId,
|
|
593
593
|
payload: {
|
|
594
|
-
content: `⏱
|
|
594
|
+
content: `⏱ Approval not received in time — run stopped, ${needingApproval.length} task(s) cancelled. Ask again whenever you're ready.`,
|
|
595
595
|
},
|
|
596
596
|
}));
|
|
597
597
|
return { ok: false, stalled: true, reason: 'awaiting_approval', completed: sessionActivities(session), failures };
|
|
@@ -865,7 +865,7 @@ test('runRuntimeParallelPlan skips work stuck behind a failed dependency instead
|
|
|
865
865
|
// tâche disparaître sans savoir pourquoi.
|
|
866
866
|
assert.match(session.headlessPlan[1].error.message, /\ba\b/);
|
|
867
867
|
assert.equal(session.agentEvents.some((event) => event.type === 'assistant_message'
|
|
868
|
-
&& /
|
|
868
|
+
&& /Approval required/.test(event.payload?.content ?? '')), false);
|
|
869
869
|
assert.equal(session.agentEvents.some((event) => event.type === 'plan_step_updated'
|
|
870
870
|
&& event.payload?.status === 'skipped'), true);
|
|
871
871
|
});
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { openSync, readSync, closeSync, fstatSync } from 'node:fs';
|
|
2
2
|
import { isAbsolute, join, normalize, resolve } from 'node:path';
|
|
3
3
|
import { createAgentEvent, dispatchAgentEvent } from '../core/agentEvents.js';
|
|
4
|
-
import { extractActivity, parseJsonText, sessionActivities } from '../core/activity.js';
|
|
4
|
+
import { extractActivity, mergePolledActivity, parseJsonText, sessionActivities } from '../core/activity.js';
|
|
5
5
|
import { callMcpTool, formatMcpToolResult } from '../core/mcp.js';
|
|
6
6
|
import { normalizeRuntimeLog } from '../core/runtimeLog.js';
|
|
7
7
|
import { startNextQueuedJob, syncQueueWithActivity } from '../core/jobQueue.js';
|
|
@@ -107,10 +107,10 @@ export async function pollActivitiesOnce(session, {
|
|
|
107
107
|
try {
|
|
108
108
|
const result = await callTool(session.mcp, activity.poll.server, activity.poll.tool, activity.poll.args ?? {}, signal);
|
|
109
109
|
const payload = parseJsonText(formatMcpToolResult(result));
|
|
110
|
-
const polledActivity = extractActivity(payload, {
|
|
110
|
+
const polledActivity = mergePolledActivity(activity, extractActivity(payload, {
|
|
111
111
|
server: activity.poll.server,
|
|
112
112
|
tool: activity.poll.tool,
|
|
113
|
-
});
|
|
113
|
+
}));
|
|
114
114
|
if (polledActivity) {
|
|
115
115
|
dispatchAgentEvent(session, createAgentEvent('activity_upserted', {
|
|
116
116
|
origin: 'runtime_poll',
|
package/src/shell/RightPane.tsx
CHANGED
|
@@ -20,10 +20,11 @@ type LogLineParts = { time: string | null; message: string };
|
|
|
20
20
|
// 4 slots (was 6): items can now span up to 5 lines each (wrapped label +
|
|
21
21
|
// wrapped status/error), so fewer, readable entries beat more, truncated ones.
|
|
22
22
|
const ACTIVITY_SLOTS = Array.from({ length: 4 }, (_, index) => index);
|
|
23
|
-
// Hauteur du panneau Plan : 6 lignes visibles
|
|
24
|
-
//
|
|
25
|
-
//
|
|
26
|
-
|
|
23
|
+
// Hauteur MINIMALE du panneau Plan : 6 lignes visibles. Le panneau occupe
|
|
24
|
+
// desormais tout l'espace libre jusqu'au panneau Runtime/Agent status (il ne
|
|
25
|
+
// reste plus de trou de 6 lignes entre les deux) ; la scrollbox reste
|
|
26
|
+
// defilable des que le plan depasse cette fenetre.
|
|
27
|
+
const PLAN_MIN_VIEWPORT_ROWS = 6;
|
|
27
28
|
|
|
28
29
|
function wrapLine(value: string, width: number) {
|
|
29
30
|
const max = Math.max(8, width);
|
|
@@ -215,12 +216,11 @@ export function PlanPanel(props: { plan: PlanStep[]; width: number; jobName?: st
|
|
|
215
216
|
total + wrapLine(`${icon(step.status)} ${step.step}. ${step.description}`, stepTextWidth(step)).slice(0, 2).length, 0));
|
|
216
217
|
const title = () => {
|
|
217
218
|
const label = props.jobName ? `Plan : ${props.jobName}` : 'Plan';
|
|
218
|
-
return visualRows() >
|
|
219
|
+
return visualRows() > PLAN_MIN_VIEWPORT_ROWS ? `${label} (${props.plan.length}) · scroll` : label;
|
|
219
220
|
};
|
|
220
|
-
const viewportRows = () => Math.min(PLAN_VIEWPORT_ROWS, Math.max(1, visualRows()));
|
|
221
221
|
const summaryLines = () => props.summary ? wrapLine(props.summary, lineWidth()).slice(0, 2) : [];
|
|
222
222
|
return (
|
|
223
|
-
<box flexShrink={
|
|
223
|
+
<box flexGrow={1} flexShrink={1} minHeight={PLAN_MIN_VIEWPORT_ROWS + 2} flexDirection="column" padding={1}>
|
|
224
224
|
<text width={lineWidth()} fg="#D6DEE8" content={fit(title(), lineWidth())} />
|
|
225
225
|
<Show when={summaryLines().length > 0}>
|
|
226
226
|
<box flexShrink={0} flexDirection="column">
|
|
@@ -230,13 +230,15 @@ export function PlanPanel(props: { plan: PlanStep[]; width: number; jobName?: st
|
|
|
230
230
|
</box>
|
|
231
231
|
</Show>
|
|
232
232
|
<scrollbox
|
|
233
|
-
|
|
233
|
+
flexGrow={1}
|
|
234
|
+
flexShrink={1}
|
|
235
|
+
minHeight={Math.min(PLAN_MIN_VIEWPORT_ROWS, Math.max(1, visualRows()))}
|
|
234
236
|
focusable={false}
|
|
235
237
|
scrollY={true}
|
|
236
238
|
scrollX={false}
|
|
237
239
|
stickyStart="top"
|
|
238
240
|
viewportCulling={true}
|
|
239
|
-
verticalScrollbarOptions={{ visible: visualRows() >
|
|
241
|
+
verticalScrollbarOptions={{ visible: visualRows() > PLAN_MIN_VIEWPORT_ROWS }}
|
|
240
242
|
>
|
|
241
243
|
<Index each={props.plan}>
|
|
242
244
|
{(step) => {
|
|
@@ -261,16 +263,23 @@ export function PlanPanel(props: { plan: PlanStep[]; width: number; jobName?: st
|
|
|
261
263
|
|
|
262
264
|
export function ActivityPanel(props: { activities: any[]; width: number }) {
|
|
263
265
|
const lineWidth = () => Math.max(8, props.width - 2);
|
|
264
|
-
const visible = () => props.activities.slice(
|
|
265
|
-
const visibleSlots = () => visible().map((_activity, index) => index);
|
|
266
|
-
const activityAt = (index: number) => visible()[index] ?? null;
|
|
266
|
+
const visible = () => props.activities.slice().reverse();
|
|
267
267
|
return (
|
|
268
|
-
<box
|
|
268
|
+
<box flexGrow={1} flexDirection="column" paddingX={1} backgroundColor="#111318">
|
|
269
269
|
<text width={lineWidth()} fg="#D6DEE8" content="Activity" />
|
|
270
270
|
<Show when={visible().length > 0} fallback={<text width={lineWidth()} fg="#7F8C8D" content="no active jobs" />}>
|
|
271
|
-
<
|
|
272
|
-
{
|
|
273
|
-
|
|
271
|
+
<scrollbox
|
|
272
|
+
flexGrow={1}
|
|
273
|
+
flexShrink={1}
|
|
274
|
+
focusable={false}
|
|
275
|
+
scrollY={true}
|
|
276
|
+
scrollX={false}
|
|
277
|
+
stickyStart="top"
|
|
278
|
+
viewportCulling={true}
|
|
279
|
+
verticalScrollbarOptions={{ visible: visible().length > 3 }}
|
|
280
|
+
>
|
|
281
|
+
<Index each={visible()}>
|
|
282
|
+
{(activity) => {
|
|
274
283
|
// Wrap instead of hard-truncating: a 40-column pane cut labels to
|
|
275
284
|
// "Appliquer la config recommandée (doct…" and hid the one thing
|
|
276
285
|
// that mattered. Labels get up to 2 lines, the status/error line
|
|
@@ -318,6 +327,7 @@ export function ActivityPanel(props: { activities: any[]; width: number }) {
|
|
|
318
327
|
);
|
|
319
328
|
}}
|
|
320
329
|
</Index>
|
|
330
|
+
</scrollbox>
|
|
321
331
|
</Show>
|
|
322
332
|
</box>
|
|
323
333
|
);
|
|
@@ -385,7 +395,11 @@ export function LogPanel(props: { logs: string[]; width: number; filter?: string
|
|
|
385
395
|
.filter((line) => activeLogTab() === 'agent-status' ? isAgentStatus(line) : !isAgentStatus(line));
|
|
386
396
|
const allLines = createMemo(() => logRenderLines(filteredLogs(), lineWidth()));
|
|
387
397
|
return (
|
|
388
|
-
|
|
398
|
+
// No hardcoded marginTop here any more: the 6 blank lines it reserved
|
|
399
|
+
// above the Runtime/Agent status tabs left a dead gap under a short Plan
|
|
400
|
+
// and made the two panels look detached. The Plan/Queue panel now grows
|
|
401
|
+
// into that space instead, so its bottom edge meets this panel's header.
|
|
402
|
+
<box flexGrow={2} flexDirection="column" paddingX={1} focusable={false}>
|
|
389
403
|
<text width={lineWidth()} fg="#4B5563" content={'─'.repeat(lineWidth())} />
|
|
390
404
|
<box height={1} flexDirection="row">
|
|
391
405
|
<text
|
|
@@ -530,8 +544,8 @@ export function RightPane(props: {
|
|
|
530
544
|
<TabHeader active={props.activeTab} queueCount={props.queueInfo.active} onTabClick={props.onTabClick} />
|
|
531
545
|
<Show when={props.pendingApprovals.length > 0}>
|
|
532
546
|
<box height={2} flexDirection="column" border={['left']} borderStyle="heavy" borderColor="#FBBF24" paddingX={1}>
|
|
533
|
-
<text fg="#FBBF24" content={`${props.pendingApprovals.length}
|
|
534
|
-
<text fg="#0B1020" bg="#FBBF24" content="
|
|
547
|
+
<text fg="#FBBF24" content={`${props.pendingApprovals.length} approval(s) required`} />
|
|
548
|
+
<text fg="#0B1020" bg="#FBBF24" content=" Approve run " onMouseUp={props.onApprove} />
|
|
535
549
|
</box>
|
|
536
550
|
</Show>
|
|
537
551
|
<Show when={props.activeTab === 'queue'} fallback={(
|
package/src/shell/repl.js
CHANGED
|
@@ -11,7 +11,7 @@ import { markedTerminal } from 'marked-terminal';
|
|
|
11
11
|
import { buildAgentSystemPrompt, formatLlmUnavailableMessage, isOrchestrationBypassTool } from '../agent/graph.js';
|
|
12
12
|
import { handleSlashCommand, rawCommandAgentPrompt } from '../commands/slash.js';
|
|
13
13
|
import { serviceChoices as composeServiceChoices, serviceDescription } from '../core/compose.js';
|
|
14
|
-
import { extractActivity, parseJsonText, sessionActivities } from '../core/activity.js';
|
|
14
|
+
import { extractActivity, mergePolledActivity, parseJsonText, sessionActivities } from '../core/activity.js';
|
|
15
15
|
import { syncActivitiesToPlan } from '../core/plan.js';
|
|
16
16
|
import { buildLlmTools, callMcpTool, formatMcpToolResult, parseToolCallName, resolveToolCallName } from '../core/mcp.js';
|
|
17
17
|
import { runBoundedToolLoop } from '../core/toolLoop.js';
|
|
@@ -127,12 +127,12 @@ const SUBCOMMAND_COMPLETION_DESCRIPTIONS = {
|
|
|
127
127
|
export function runtimeUnavailableReason(runtime) {
|
|
128
128
|
if (runtime?.url) return null;
|
|
129
129
|
const reason = runtime?.error ?? runtime?.unavailableReason ?? runtime?.reason ?? null;
|
|
130
|
-
return reason ? String(reason) : 'runtime
|
|
130
|
+
return reason ? String(reason) : 'runtime unavailable';
|
|
131
131
|
}
|
|
132
132
|
|
|
133
133
|
export function runtimeUnavailableAgentMessage(runtime) {
|
|
134
134
|
const reason = runtimeUnavailableReason(runtime);
|
|
135
|
-
return reason ? `⚠ Runtime
|
|
135
|
+
return reason ? `⚠ Runtime unavailable: ${reason} — /agent disabled, /chat still available` : null;
|
|
136
136
|
}
|
|
137
137
|
|
|
138
138
|
export function runtimeStatusLine(runtime, session) {
|
|
@@ -145,7 +145,7 @@ export function runtimeStatusLine(runtime, session) {
|
|
|
145
145
|
export function recordRuntimeUnavailableAgentInput(session, line, runtime) {
|
|
146
146
|
const message = runtimeUnavailableAgentMessage(runtime);
|
|
147
147
|
conversationMessages(session).push({ role: 'user', content: line });
|
|
148
|
-
conversationMessages(session).push({ role: 'command', content: message ?? 'Runtime
|
|
148
|
+
conversationMessages(session).push({ role: 'command', content: message ?? 'Runtime unavailable.' });
|
|
149
149
|
return message;
|
|
150
150
|
}
|
|
151
151
|
|
|
@@ -1422,10 +1422,10 @@ async function runAgentTurn(input, {
|
|
|
1422
1422
|
if (donnaMessage) {
|
|
1423
1423
|
donnaMessage.content = stripDsmlArtifacts(donnaMessage.content).trimEnd();
|
|
1424
1424
|
if (!donnaMessage.content.trim()) {
|
|
1425
|
-
donnaMessage.content = formatLlmUnavailableMessage('
|
|
1425
|
+
donnaMessage.content = formatLlmUnavailableMessage('empty stream');
|
|
1426
1426
|
}
|
|
1427
1427
|
} else {
|
|
1428
|
-
messages.push({ role: 'donna', content: formatLlmUnavailableMessage('
|
|
1428
|
+
messages.push({ role: 'donna', content: formatLlmUnavailableMessage('empty stream') });
|
|
1429
1429
|
}
|
|
1430
1430
|
onUpdate?.();
|
|
1431
1431
|
return {};
|
|
@@ -1464,7 +1464,7 @@ async function runAgentTurn(input, {
|
|
|
1464
1464
|
}
|
|
1465
1465
|
donnaMessage.content = stripDsmlArtifacts(donnaMessage.content).trimEnd();
|
|
1466
1466
|
if (!donnaMessage.content.trim()) {
|
|
1467
|
-
donnaMessage.content = formatLlmUnavailableMessage('
|
|
1467
|
+
donnaMessage.content = formatLlmUnavailableMessage('empty stream');
|
|
1468
1468
|
onUpdate?.();
|
|
1469
1469
|
}
|
|
1470
1470
|
} catch (err) {
|
|
@@ -1481,9 +1481,9 @@ async function runAgentTurn(input, {
|
|
|
1481
1481
|
}
|
|
1482
1482
|
|
|
1483
1483
|
if (donnaMessage) {
|
|
1484
|
-
donnaMessage.content = formatLlmUnavailableMessage('
|
|
1484
|
+
donnaMessage.content = formatLlmUnavailableMessage('empty response');
|
|
1485
1485
|
} else {
|
|
1486
|
-
messages.push({ role: 'donna', content: formatLlmUnavailableMessage('
|
|
1486
|
+
messages.push({ role: 'donna', content: formatLlmUnavailableMessage('empty response') });
|
|
1487
1487
|
}
|
|
1488
1488
|
onUpdate?.();
|
|
1489
1489
|
return {};
|
|
@@ -1533,8 +1533,8 @@ async function runChatToolLoop({ input, session, history, donnaMessage, onUpdate
|
|
|
1533
1533
|
onTextReset,
|
|
1534
1534
|
});
|
|
1535
1535
|
donnaMessage.content = capped
|
|
1536
|
-
? '
|
|
1537
|
-
: (stripDsmlArtifacts(content).trimEnd() || formatLlmUnavailableMessage('
|
|
1536
|
+
? 'Could not finish within the chat mode iteration limit. Switch to /agent if needed.'
|
|
1537
|
+
: (stripDsmlArtifacts(content).trimEnd() || formatLlmUnavailableMessage('empty response'));
|
|
1538
1538
|
onUpdate?.();
|
|
1539
1539
|
}
|
|
1540
1540
|
|
|
@@ -1580,7 +1580,7 @@ async function runDirectChatTurn(input, { session, onUpdate, onStep }) {
|
|
|
1580
1580
|
}
|
|
1581
1581
|
donnaMessage.content = stripDsmlArtifacts(donnaMessage.content).trimEnd();
|
|
1582
1582
|
if (!donnaMessage.content.trim()) {
|
|
1583
|
-
donnaMessage.content = formatLlmUnavailableMessage('
|
|
1583
|
+
donnaMessage.content = formatLlmUnavailableMessage('empty stream');
|
|
1584
1584
|
onUpdate?.();
|
|
1585
1585
|
}
|
|
1586
1586
|
}
|
|
@@ -1639,7 +1639,7 @@ export async function runHeadlessChatTurn(session, input, { history = [], onStep
|
|
|
1639
1639
|
const clean = stripDsmlArtifacts(delta);
|
|
1640
1640
|
if (clean) { content += clean; onTextDelta?.(clean); }
|
|
1641
1641
|
}
|
|
1642
|
-
return stripDsmlArtifacts(content).trimEnd() || formatLlmUnavailableMessage('
|
|
1642
|
+
return stripDsmlArtifacts(content).trimEnd() || formatLlmUnavailableMessage('empty stream');
|
|
1643
1643
|
}
|
|
1644
1644
|
return directChatUnavailableText(session);
|
|
1645
1645
|
}
|
|
@@ -1948,7 +1948,7 @@ async function runTuiShell({ agent, packageJson, session, runtime = null }) {
|
|
|
1948
1948
|
void callMcpTool(session.mcp, activity.poll.server, activity.poll.tool, activity.poll.args ?? {})
|
|
1949
1949
|
.then((result) => {
|
|
1950
1950
|
const payload = parseJsonText(formatMcpToolResult(result));
|
|
1951
|
-
const polledActivity = extractActivity(payload, { server: activity.poll.server, tool: activity.poll.tool });
|
|
1951
|
+
const polledActivity = mergePolledActivity(activity, extractActivity(payload, { server: activity.poll.server, tool: activity.poll.tool }));
|
|
1952
1952
|
if (polledActivity) {
|
|
1953
1953
|
dispatchAgentEvent(session, createAgentEvent('activity_upserted', {
|
|
1954
1954
|
origin: 'poll',
|
package/src/shell/repl.test.js
CHANGED
|
@@ -114,7 +114,7 @@ test('Activity uses only visible jobs and leaves remaining height to Flow/Trace'
|
|
|
114
114
|
source.indexOf('export function ActivityPanel'),
|
|
115
115
|
source.indexOf('type LogSegment'),
|
|
116
116
|
);
|
|
117
|
-
assert.match(activityPanel, /<Index each=\{
|
|
117
|
+
assert.match(activityPanel, /<Index each=\{visible\(\)\}>/);
|
|
118
118
|
assert.doesNotMatch(activityPanel, /<Index each=\{ACTIVITY_SLOTS\}>/);
|
|
119
119
|
assert.match(activityPanel, /paddingX=\{1\}/);
|
|
120
120
|
assert.doesNotMatch(activityPanel, /updatedLine\(activity\(\)\)/);
|
|
@@ -144,7 +144,7 @@ test('ShellUI launcher never starts agents or workspace services implicitly', as
|
|
|
144
144
|
test('ShellUI lets the right pane extend to the terminal edge', async () => {
|
|
145
145
|
const tui = await readFile(new URL('./tui.tsx', import.meta.url), 'utf8');
|
|
146
146
|
const pane = await readFile(new URL('./RightPane.tsx', import.meta.url), 'utf8');
|
|
147
|
-
assert.match(tui, /Math\.
|
|
147
|
+
assert.match(tui, /Math\.max\(32, Math\.floor\(width \* 0\.38\) \+ 2\)/);
|
|
148
148
|
assert.match(pane, /paddingLeft=\{1\}/);
|
|
149
149
|
assert.doesNotMatch(pane, /height="100%" flexDirection="column" padding=\{1\}/);
|
|
150
150
|
});
|
|
@@ -742,7 +742,7 @@ test('agent mode without runtime records a visible error instead of falling back
|
|
|
742
742
|
|
|
743
743
|
const message = recordRuntimeUnavailableAgentInput(session, 'salut', { error: 'port 7788 already in use' });
|
|
744
744
|
|
|
745
|
-
assert.equal(message, '⚠ Runtime
|
|
745
|
+
assert.equal(message, '⚠ Runtime unavailable: port 7788 already in use — /agent disabled, /chat still available');
|
|
746
746
|
// `at` est posé à l'insertion : on compare le reste.
|
|
747
747
|
assert.deepEqual(
|
|
748
748
|
conversationMessages(session).map(({ at, ...rest }) => rest),
|
|
@@ -760,7 +760,7 @@ test('runtime status exposes the disconnected reason', () => {
|
|
|
760
760
|
);
|
|
761
761
|
assert.equal(
|
|
762
762
|
runtimeUnavailableAgentMessage({ error: 'token mismatch' }),
|
|
763
|
-
'⚠ Runtime
|
|
763
|
+
'⚠ Runtime unavailable: token mismatch — /agent disabled, /chat still available',
|
|
764
764
|
);
|
|
765
765
|
});
|
|
766
766
|
|
|
@@ -806,7 +806,7 @@ test('/queue cancel on a runtime workflow id points to run cancellation commands
|
|
|
806
806
|
});
|
|
807
807
|
|
|
808
808
|
assert.equal(result.exit, false);
|
|
809
|
-
assert.match(conversationMessages(session).at(-1).content, /
|
|
809
|
+
assert.match(conversationMessages(session).at(-1).content, /Runtime-managed item/);
|
|
810
810
|
assert.match(conversationMessages(session).at(-1).content, /\/run kill/);
|
|
811
811
|
});
|
|
812
812
|
|
package/src/shell/tui.tsx
CHANGED
|
@@ -156,7 +156,7 @@ function App(props: {
|
|
|
156
156
|
const exitShell = () => {
|
|
157
157
|
if (exiting) return;
|
|
158
158
|
exiting = true;
|
|
159
|
-
setExitStatus('
|
|
159
|
+
setExitStatus('Shutting down…');
|
|
160
160
|
const task = Promise.resolve().then(async () => {
|
|
161
161
|
renderer.destroy();
|
|
162
162
|
console.log('[wiki-manager] shell closed; shared runtime left running.');
|
|
@@ -180,11 +180,11 @@ function App(props: {
|
|
|
180
180
|
const conversationRows = createMemo(() => Math.max(4, dimensions().height - 5 - chatInputHeight() - 4));
|
|
181
181
|
const rightColumns = createMemo(() => {
|
|
182
182
|
const width = dimensions().width;
|
|
183
|
-
// 38% + 2 columns
|
|
184
|
-
//
|
|
185
|
-
//
|
|
186
|
-
//
|
|
187
|
-
return Math.max(32, Math.
|
|
183
|
+
// 38% + 2 columns: the Plan/Activity/Logs panes carry job labels, file
|
|
184
|
+
// names and error messages. The pane scales with the terminal instead of
|
|
185
|
+
// being capped, so a wide screen widens the detail pane rather than leaving
|
|
186
|
+
// it narrow while the conversation column absorbs all the extra slack.
|
|
187
|
+
return Math.max(32, Math.floor(width * 0.38) + 2);
|
|
188
188
|
});
|
|
189
189
|
const leftColumns = createMemo(() => Math.max(32, dimensions().width - rightColumns() - 1));
|
|
190
190
|
const conversationColumns = createMemo(() => {
|
package/src/shell/useAgent.ts
CHANGED
|
@@ -52,7 +52,7 @@ export function useAgent(props: { agent: unknown; packageJson: Record<string, un
|
|
|
52
52
|
}
|
|
53
53
|
if (!props.chatMode() && !trimmed.startsWith('/') && !freeTextRouting?.local) {
|
|
54
54
|
const message = recordRuntimeUnavailableAgentInput(props.session, trimmed, {
|
|
55
|
-
error: props.runtimeUnavailableReason ?? 'runtime
|
|
55
|
+
error: props.runtimeUnavailableReason ?? 'runtime unavailable',
|
|
56
56
|
});
|
|
57
57
|
props.addLog(message ?? 'runtime: disconnected');
|
|
58
58
|
props.refresh();
|
package/src/shell/useSession.ts
CHANGED
|
@@ -3,7 +3,7 @@ import { isAbsolute, relative, resolve, sep } from 'node:path';
|
|
|
3
3
|
import { createEffect, createMemo, createSignal, onCleanup } from 'solid-js';
|
|
4
4
|
import { formatMcpToolResult, callMcpTool } from '../core/mcp.js';
|
|
5
5
|
import { versionWithBuild } from '../core/buildInfo.js';
|
|
6
|
-
import { extractActivity, parseJsonText, sessionActivities } from '../core/activity.js';
|
|
6
|
+
import { extractActivity, mergePolledActivity, parseJsonText, sessionActivities } from '../core/activity.js';
|
|
7
7
|
import { formatPlanStatus, formatCompletedActivities, formatPlanStep } from '../core/plan.js';
|
|
8
8
|
import { createAgentEvent, dispatchAgentEvent } from '../core/agentEvents.js';
|
|
9
9
|
import { projectQueue, queueCounts, startNextQueuedJob, syncQueueWithActivity } from '../core/jobQueue.js';
|
|
@@ -158,7 +158,7 @@ export function useSession(props: { agent: unknown; packageJson: Record<string,
|
|
|
158
158
|
const runtimeHint = createMemo(() => {
|
|
159
159
|
version();
|
|
160
160
|
if (props.runtime?.url) return null;
|
|
161
|
-
return runtimeUnavailableAgentMessage({ error: runtimeUnavailableReason() ?? 'runtime
|
|
161
|
+
return runtimeUnavailableAgentMessage({ error: runtimeUnavailableReason() ?? 'runtime unavailable' });
|
|
162
162
|
});
|
|
163
163
|
const matchContext = createMemo(() => {
|
|
164
164
|
if (input() === dismissedSlashInput()) return null;
|
|
@@ -277,7 +277,7 @@ export function useSession(props: { agent: unknown; packageJson: Record<string,
|
|
|
277
277
|
return {
|
|
278
278
|
...item,
|
|
279
279
|
id: item.id,
|
|
280
|
-
label: `${chain.skillName ?? 'skill'}${chain.
|
|
280
|
+
label: `${chain.skillName ?? 'skill'}${chain.selectionLabel ? ` [${chain.selectionLabel}]` : ''} ${position} · ${step.label}${reason}`,
|
|
281
281
|
status: item.status,
|
|
282
282
|
_runtime: true,
|
|
283
283
|
_control: true,
|
|
@@ -602,10 +602,10 @@ export function useSession(props: { agent: unknown; packageJson: Record<string,
|
|
|
602
602
|
void callMcpTool(session.mcp, activity.poll.server, activity.poll.tool, activity.poll.args ?? {})
|
|
603
603
|
.then((result) => {
|
|
604
604
|
const payload = parseJsonText(formatMcpToolResult(result));
|
|
605
|
-
const polledActivity = extractActivity(payload, {
|
|
605
|
+
const polledActivity = mergePolledActivity(activity, extractActivity(payload, {
|
|
606
606
|
server: activity.poll.server,
|
|
607
607
|
tool: activity.poll.tool,
|
|
608
|
-
});
|
|
608
|
+
}));
|
|
609
609
|
if (polledActivity) {
|
|
610
610
|
dispatchAgentEvent(session, createAgentEvent('activity_upserted', {
|
|
611
611
|
origin: 'poll',
|