@dotdrelle/wiki-manager 0.15.43 → 0.15.48
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 +92 -24
- package/mcp.endpoints.example.json +1 -1
- package/package.json +2 -2
- package/src/agent/graph.js +288 -22
- package/src/agent/graph.test.js +551 -1
- package/src/agent/skillRecursion.test.js +98 -0
- package/src/cli/wiki-manager.js +209 -7
- package/src/cli/wiki-manager.test.js +89 -0
- package/src/commands/slash.js +28 -10
- package/src/contracts/schemas.js +1 -1
- package/src/core/agentEvents.js +50 -0
- package/src/core/agentEvents.test.js +52 -0
- package/src/core/buildInfo.json +2 -2
- package/src/core/env.js +20 -1
- package/src/core/env.test.js +34 -0
- package/src/core/mcp.js +1 -1
- package/src/core/runtimeLog.js +15 -0
- package/src/core/runtimeLog.test.js +15 -1
- package/src/core/skillChainView.js +84 -0
- package/src/core/skillChainView.test.js +50 -0
- package/src/core/skillCompiler.js +135 -0
- package/src/core/skillCompiler.test.js +91 -0
- package/src/core/skillInvocation.js +79 -0
- package/src/core/skillInvocation.test.js +73 -0
- package/src/core/skills.js +81 -19
- package/src/runtime/client.js +45 -4
- package/src/runtime/controlCancellation.js +33 -0
- package/src/runtime/controlCancellation.test.js +49 -0
- package/src/runtime/controlDrain.js +50 -0
- package/src/runtime/controlDrain.test.js +38 -0
- package/src/runtime/server.js +326 -15
- package/src/runtime/server.test.js +341 -0
- package/src/runtime/skillChain.e2e.test.js +394 -0
- package/src/runtime/skillRun.js +104 -0
- package/src/runtime/skillRun.test.js +84 -0
- package/src/runtime/store.js +69 -0
- package/src/runtime/store.test.js +11 -0
- package/src/shell/RightPane.tsx +3 -2
- package/src/shell/repl.js +40 -5
- package/src/shell/repl.test.js +41 -0
- package/src/shell/useSession.ts +43 -9
package/src/runtime/server.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { createServer } from 'node:http';
|
|
2
|
-
import { randomUUID, timingSafeEqual } from 'node:crypto';
|
|
2
|
+
import { createHash, randomUUID, timingSafeEqual } from 'node:crypto';
|
|
3
3
|
import { conversationEventSequences, createAgentEvent, dispatchAgentEvent, resetSessionProjection, reduceAgentEvents } from '../core/agentEvents.js';
|
|
4
4
|
import { activeCacertPath } from '../core/cacert.js';
|
|
5
5
|
import { normalizePlanPatch, rebasePlanPatch } from '../core/planPatch.js';
|
|
@@ -8,6 +8,23 @@ import { runtimeTokenFromEnv } from './auth.js';
|
|
|
8
8
|
import { controlMessage } from './controlMessages.js';
|
|
9
9
|
import { tasksAwaitingApproval } from '../orchestrator/dependencyResolver.js';
|
|
10
10
|
import { approvalClassForTask } from '../orchestrator/approvalPolicy.js';
|
|
11
|
+
import { matchSkillInvocation } from '../core/skillInvocation.js';
|
|
12
|
+
import { reconcileControlQueue } from './controlDrain.js';
|
|
13
|
+
import { cancelControlChain, cancelQueuedControlItem } from './controlCancellation.js';
|
|
14
|
+
import { runSkillChain } from './skillRun.js';
|
|
15
|
+
import { findSkill, listSkills } from '../core/skills.js';
|
|
16
|
+
|
|
17
|
+
const PRIVATE_CONTROL_INPUTS = new WeakMap();
|
|
18
|
+
|
|
19
|
+
function privateControlInputsFor(session) {
|
|
20
|
+
if (!session || (typeof session !== 'object' && typeof session !== 'function')) return new Map();
|
|
21
|
+
let inputs = PRIVATE_CONTROL_INPUTS.get(session);
|
|
22
|
+
if (!inputs) {
|
|
23
|
+
inputs = new Map();
|
|
24
|
+
PRIVATE_CONTROL_INPUTS.set(session, inputs);
|
|
25
|
+
}
|
|
26
|
+
return inputs;
|
|
27
|
+
}
|
|
11
28
|
|
|
12
29
|
export function startRuntimeServer({
|
|
13
30
|
host = '127.0.0.1',
|
|
@@ -31,6 +48,8 @@ export function startRuntimeServer({
|
|
|
31
48
|
exitOnShutdown = process.env.WIKI_MANAGER_RUNTIME_CHILD === '1',
|
|
32
49
|
} = {}) {
|
|
33
50
|
const clients = new Set();
|
|
51
|
+
// Compiled objectives are private execution material. They deliberately do
|
|
52
|
+
// not enter events, projections, SSE, audit output or the runs table.
|
|
34
53
|
// When this runtime process started — used by ensureRuntime to detect that
|
|
35
54
|
// the manager source has been edited since (dev staleness) and auto-restart.
|
|
36
55
|
const runtimeStartedAtMs = Date.now();
|
|
@@ -200,6 +219,21 @@ export function startRuntimeServer({
|
|
|
200
219
|
});
|
|
201
220
|
return;
|
|
202
221
|
}
|
|
222
|
+
if (action === 'cancel_item') {
|
|
223
|
+
const itemId = String(body.itemId ?? body.id ?? '').trim();
|
|
224
|
+
if (!itemId) {
|
|
225
|
+
sendJson(response, 400, { error: 'Missing itemId.' });
|
|
226
|
+
return;
|
|
227
|
+
}
|
|
228
|
+
validateContractInDev('controlMessage', { ...body, action, id: itemId });
|
|
229
|
+
const result = cancelQueuedControlItem(context.session, itemId, {
|
|
230
|
+
cancelItem: (item, reason) => emitControlCancelled(context, item, reason),
|
|
231
|
+
skipItem: (item, reason) => emitControlSkipped(context, item, reason),
|
|
232
|
+
});
|
|
233
|
+
void drainControlQueue(context);
|
|
234
|
+
sendJson(response, result.cancelled ? 202 : 200, { ...result, ...controlStatus(context, store) });
|
|
235
|
+
return;
|
|
236
|
+
}
|
|
203
237
|
sendJson(response, 400, { error: 'Unsupported control action.' });
|
|
204
238
|
return;
|
|
205
239
|
}
|
|
@@ -282,6 +316,45 @@ export function startRuntimeServer({
|
|
|
282
316
|
return;
|
|
283
317
|
}
|
|
284
318
|
validateContractInDev('runRequest', { ...body, input });
|
|
319
|
+
const explicitSkillName = String(body.skillName ?? '').trim().toLowerCase();
|
|
320
|
+
const invokedSkillName = /^\/([A-Za-z0-9_-]+)/.exec(input)?.[1]?.toLowerCase() ?? '';
|
|
321
|
+
if (body.skillArguments !== undefined && explicitSkillName !== invokedSkillName) {
|
|
322
|
+
sendJson(response, 400, { ok: false, terminal: true, code: 'skill_name_mismatch' });
|
|
323
|
+
return;
|
|
324
|
+
}
|
|
325
|
+
// This structured API call is itself the caller's explicit skill
|
|
326
|
+
// designation, including for reserved names. The equality check
|
|
327
|
+
// above prevents `skillName: status` from turning unrelated prose
|
|
328
|
+
// into the homonymous skill. Model-originated calls additionally
|
|
329
|
+
// pass explicitSkillReference() before reaching this endpoint.
|
|
330
|
+
if (explicitSkillName && body.skillArguments !== undefined) {
|
|
331
|
+
const result = await context.session._runSkillWithinRun(explicitSkillName, body.skillArguments, {
|
|
332
|
+
idempotencyKey: body.idempotencyKey ?? null,
|
|
333
|
+
turnId: body.turnId ?? null,
|
|
334
|
+
selectionKind: body.selectionKind ?? null,
|
|
335
|
+
// Pile de l'appelant : c'est le seul canal par lequel elle peut
|
|
336
|
+
// franchir la frontière HTTP.
|
|
337
|
+
skillStack: Array.isArray(body.skillStack) ? body.skillStack : [],
|
|
338
|
+
});
|
|
339
|
+
sendJson(response, result.accepted ? 202 : skillResultErrorStatus(result), result);
|
|
340
|
+
return;
|
|
341
|
+
}
|
|
342
|
+
const skillMatch = matchSkillInvocation(context.session, input, {
|
|
343
|
+
// Reserved names are unlocked only by a matching structured skill
|
|
344
|
+
// invocation, never by an unrelated truthy marker in the body.
|
|
345
|
+
allowReserved: Boolean(explicitSkillName && explicitSkillName === invokedSkillName),
|
|
346
|
+
});
|
|
347
|
+
if (skillMatch) {
|
|
348
|
+
try {
|
|
349
|
+
const result = await enqueueSkillInvocation(context, skillMatch);
|
|
350
|
+
sendJson(response, 202, { accepted: true, kind: 'skill_chain', ...result, ...controlStatus(context, store) });
|
|
351
|
+
} catch (err) {
|
|
352
|
+
const error = skillInvocationErrorMessage(err);
|
|
353
|
+
publishSkillInvocationFailure(context, input, error);
|
|
354
|
+
sendJson(response, skillInvocationErrorStatus(err), { error, code: err?.code ?? 'skill_compile_failed' });
|
|
355
|
+
}
|
|
356
|
+
return;
|
|
357
|
+
}
|
|
285
358
|
if (context.running) {
|
|
286
359
|
// A structured capability plan must never degrade into a free-text
|
|
287
360
|
// control message: its exact arguments and approval policy would
|
|
@@ -339,6 +412,23 @@ export function startRuntimeServer({
|
|
|
339
412
|
// run is active. Other interactive turns still become control
|
|
340
413
|
// messages so they cannot start a competing agent decision.
|
|
341
414
|
const readOnlyChat = String(body.mode ?? '').toLowerCase() === 'chat';
|
|
415
|
+
// An explicit /skill invocation has deterministic meaning. Keep the
|
|
416
|
+
// conversational /turn boundary, but do not ask the LLM to rediscover
|
|
417
|
+
// the skill from prose: it could choose a direct mutation instead and
|
|
418
|
+
// silently bypass the private workflow. Informational prose merely
|
|
419
|
+
// mentioning a skill never matches this anchored invocation parser.
|
|
420
|
+
const skillMatch = !readOnlyChat ? matchSkillInvocation(context.session, input) : null;
|
|
421
|
+
if (skillMatch) {
|
|
422
|
+
try {
|
|
423
|
+
const result = await enqueueSkillInvocation(context, skillMatch);
|
|
424
|
+
sendJson(response, 202, { accepted: true, kind: 'skill_chain', ...result, ...controlStatus(context, store) });
|
|
425
|
+
} catch (err) {
|
|
426
|
+
const error = skillInvocationErrorMessage(err);
|
|
427
|
+
publishSkillInvocationFailure(context, input, error);
|
|
428
|
+
sendJson(response, skillInvocationErrorStatus(err), { error, code: err?.code ?? 'skill_compile_failed' });
|
|
429
|
+
}
|
|
430
|
+
return;
|
|
431
|
+
}
|
|
342
432
|
if (context.running && !readOnlyChat) {
|
|
343
433
|
const result = await handleControlMessage(context, store, input, {
|
|
344
434
|
intent: body.intent,
|
|
@@ -436,6 +526,10 @@ export function startRuntimeServer({
|
|
|
436
526
|
sendJson(response, 200, { cancelled: false, reason: 'no active run' });
|
|
437
527
|
return;
|
|
438
528
|
}
|
|
529
|
+
cancelControlChain(context.session, {
|
|
530
|
+
runId: context.currentRunId,
|
|
531
|
+
cancelItem: (item, reason) => emitControlSkipped(context, item, reason),
|
|
532
|
+
});
|
|
439
533
|
context.currentAbortController.abort();
|
|
440
534
|
await cancel?.(context);
|
|
441
535
|
sendJson(response, 202, { cancelled: true, workspace: context.workspace ?? workspace ?? null });
|
|
@@ -561,7 +655,7 @@ export function startRuntimeServer({
|
|
|
561
655
|
host,
|
|
562
656
|
port: typeof address === 'object' && address ? address.port : port,
|
|
563
657
|
publish,
|
|
564
|
-
drainControl: (context) =>
|
|
658
|
+
drainControl: (context) => drainControlQueue(context),
|
|
565
659
|
close: () => new Promise((closeResolve, closeReject) => {
|
|
566
660
|
for (const client of clients) client.response.end();
|
|
567
661
|
clients.clear();
|
|
@@ -572,7 +666,70 @@ export function startRuntimeServer({
|
|
|
572
666
|
});
|
|
573
667
|
|
|
574
668
|
async function resolveContext({ workspace = null } = {}) {
|
|
575
|
-
|
|
669
|
+
const context = await resolvedGetContext(workspace);
|
|
670
|
+
if (context?.session) {
|
|
671
|
+
context.session._runSkillWithinRun = async (skillName, args = {}, metadata = {}) => {
|
|
672
|
+
const skill = findSkill(context.session, skillName);
|
|
673
|
+
if (!skill) return {
|
|
674
|
+
ok: false,
|
|
675
|
+
terminal: true,
|
|
676
|
+
code: 'skill_not_found',
|
|
677
|
+
availableSkills: listSkills(context.session).map((item) => item.name),
|
|
678
|
+
};
|
|
679
|
+
try {
|
|
680
|
+
const idempotencyKey = metadata.idempotencyKey
|
|
681
|
+
? String(metadata.idempotencyKey)
|
|
682
|
+
: metadata.turnId
|
|
683
|
+
? skillIdempotencyKey(context.workspace ?? context.session.workspace, metadata.turnId, skill.name, args)
|
|
684
|
+
: null;
|
|
685
|
+
context.skillRuns ??= new Map();
|
|
686
|
+
context.pendingSkillRuns ??= new Map();
|
|
687
|
+
const persistedChainId = idempotencyKey && typeof store.findSkillRun === 'function'
|
|
688
|
+
? store.findSkillRun({ workspace: context.workspace ?? null, idempotencyKey })
|
|
689
|
+
: null;
|
|
690
|
+
const existingChainId = persistedChainId ?? (idempotencyKey ? context.skillRuns.get(idempotencyKey) : null);
|
|
691
|
+
if (existingChainId) return publicSkillRunProjection(context, skill.name, existingChainId, metadata.selectionKind, true);
|
|
692
|
+
if (idempotencyKey && context.pendingSkillRuns.has(idempotencyKey)) {
|
|
693
|
+
const pendingChainId = await context.pendingSkillRuns.get(idempotencyKey);
|
|
694
|
+
return publicSkillRunProjection(context, skill.name, pendingChainId, metadata.selectionKind, true);
|
|
695
|
+
}
|
|
696
|
+
const creation = runSkillChain(context, skill, {
|
|
697
|
+
args,
|
|
698
|
+
enqueueControlRequest,
|
|
699
|
+
drainControlQueue,
|
|
700
|
+
selectionKind: metadata.selectionKind,
|
|
701
|
+
// La pile du run appelant : elle sera empilée sur chaque élément mis
|
|
702
|
+
// en file, et c'est elle seule qui survit au hand-off.
|
|
703
|
+
skillStack: Array.isArray(metadata.skillStack) ? metadata.skillStack : [],
|
|
704
|
+
});
|
|
705
|
+
if (idempotencyKey) context.pendingSkillRuns.set(idempotencyKey, creation.then((item) => item.chainId));
|
|
706
|
+
const result = await creation;
|
|
707
|
+
if (idempotencyKey) {
|
|
708
|
+
context.skillRuns.set(idempotencyKey, result.chainId);
|
|
709
|
+
store.persistSkillRun?.({
|
|
710
|
+
workspace: context.workspace ?? context.session.workspace ?? null,
|
|
711
|
+
idempotencyKey,
|
|
712
|
+
chainId: result.chainId,
|
|
713
|
+
});
|
|
714
|
+
}
|
|
715
|
+
return publicSkillRunProjection(context, skill.name, result.chainId, metadata.selectionKind, false);
|
|
716
|
+
} catch (error) {
|
|
717
|
+
return {
|
|
718
|
+
ok: false,
|
|
719
|
+
terminal: true,
|
|
720
|
+
code: error?.code ?? 'skill_compile_failed',
|
|
721
|
+
};
|
|
722
|
+
} finally {
|
|
723
|
+
const key = metadata.idempotencyKey
|
|
724
|
+
? String(metadata.idempotencyKey)
|
|
725
|
+
: metadata.turnId
|
|
726
|
+
? skillIdempotencyKey(context.workspace ?? context.session.workspace, metadata.turnId, skill.name, args)
|
|
727
|
+
: null;
|
|
728
|
+
if (key) context.pendingSkillRuns?.delete(key);
|
|
729
|
+
}
|
|
730
|
+
};
|
|
731
|
+
}
|
|
732
|
+
return context;
|
|
576
733
|
}
|
|
577
734
|
|
|
578
735
|
// Shared by POST handlers that take a JSON body carrying an optional
|
|
@@ -649,22 +806,103 @@ export function startRuntimeServer({
|
|
|
649
806
|
context.currentRunId = null;
|
|
650
807
|
context.currentRunWorkspace = null;
|
|
651
808
|
publishState(runWorkspace, context);
|
|
652
|
-
void
|
|
809
|
+
void drainControlQueue(context);
|
|
653
810
|
});
|
|
654
811
|
return { accepted: true, runId, workspace: runWorkspace, ...(ready ? { ready } : {}) };
|
|
655
812
|
}
|
|
656
813
|
|
|
657
814
|
function startNextControlRequest(context) {
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
815
|
+
return drainControlQueue(context);
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
function drainControlQueue(context) {
|
|
819
|
+
return reconcileControlQueue(context, {
|
|
820
|
+
/*
|
|
821
|
+
La provenance de chaîne suit le run.
|
|
822
|
+
|
|
823
|
+
`chainId` et `skillName` vivaient sur l'item de contrôle et s'arrêtaient
|
|
824
|
+
là : le run ne savait pas qu'il exécutait une intention compilée depuis
|
|
825
|
+
une compétence. L'agent voyait donc un objectif métier — « ingérer les
|
|
826
|
+
fichiers en attente » — qui ressemble par construction à la description
|
|
827
|
+
de la compétence dont il sort, et la resélectionnait. En headless, où
|
|
828
|
+
personne n'interrompt, cela boucle jusqu'à épuisement du budget LLM.
|
|
829
|
+
*/
|
|
830
|
+
startItem: (item) => startRuntimeRun(context, {
|
|
831
|
+
input: takePrivateControlInput(context.session, item),
|
|
832
|
+
publicInput: item.input,
|
|
833
|
+
workspace: item.workspace ?? context.workspace ?? null,
|
|
834
|
+
// Interactive skill runs never approve their own mutations. Headless
|
|
835
|
+
// may still grant the pending run explicitly through --auto-approve.
|
|
836
|
+
...(item.chainId ? { requireApproval: true } : {}),
|
|
837
|
+
...(item.capabilityPlan !== undefined ? { capabilityPlan: item.capabilityPlan } : {}),
|
|
838
|
+
...(item.chainId
|
|
839
|
+
? {
|
|
840
|
+
skillChain: {
|
|
841
|
+
chainId: item.chainId,
|
|
842
|
+
skillName: item.skillName ?? null,
|
|
843
|
+
execution: item.skillExecution === 'direct' ? 'direct' : 'orchestrated',
|
|
844
|
+
// La pile des ancêtres, sans quoi le run ne peut pas savoir qu'il
|
|
845
|
+
// referme un cycle commencé deux hand-offs plus tôt.
|
|
846
|
+
skillStack: Array.isArray(item.skillStack) ? item.skillStack : [],
|
|
847
|
+
},
|
|
848
|
+
}
|
|
849
|
+
: {}),
|
|
850
|
+
}, { controlItemId: item.id }),
|
|
851
|
+
skipItem: (item, reason) => {
|
|
852
|
+
privateControlInputsFor(context.session).delete(item.id);
|
|
853
|
+
emitControlSkipped(context, item, reason);
|
|
854
|
+
},
|
|
855
|
+
});
|
|
667
856
|
}
|
|
857
|
+
|
|
858
|
+
function takePrivateControlInput(session, item) {
|
|
859
|
+
const privateControlInputs = privateControlInputsFor(session);
|
|
860
|
+
const input = privateControlInputs.get(item.id) ?? item.input;
|
|
861
|
+
privateControlInputs.delete(item.id);
|
|
862
|
+
return input;
|
|
863
|
+
}
|
|
864
|
+
|
|
865
|
+
async function enqueueSkillInvocation(context, match) {
|
|
866
|
+
const result = await runSkillChain(context, match.skill, {
|
|
867
|
+
rawArgs: match.rawArgs,
|
|
868
|
+
enqueueControlRequest,
|
|
869
|
+
drainControlQueue,
|
|
870
|
+
selectionKind: 'explicit_name',
|
|
871
|
+
});
|
|
872
|
+
// Publish only after compilation succeeds. The marker prevents an
|
|
873
|
+
// invocation queued during another run from inheriting that run's id.
|
|
874
|
+
dispatchAgentEvent(context.session, createAgentEvent('user_message', {
|
|
875
|
+
origin: 'user',
|
|
876
|
+
workspace: context.workspace ?? null,
|
|
877
|
+
payload: { content: `/${match.skill.name}${match.rawArgs ? ` ${match.rawArgs}` : ''}`, independent: true },
|
|
878
|
+
}));
|
|
879
|
+
return result;
|
|
880
|
+
}
|
|
881
|
+
|
|
882
|
+
function publishSkillInvocationFailure(context, input, error) {
|
|
883
|
+
const workspace = context.workspace ?? context.session?.workspace ?? null;
|
|
884
|
+
dispatchAgentEvent(context.session, createAgentEvent('user_message', {
|
|
885
|
+
origin: 'user', workspace, payload: { content: input, independent: true },
|
|
886
|
+
}));
|
|
887
|
+
dispatchAgentEvent(context.session, createAgentEvent('assistant_message', {
|
|
888
|
+
origin: 'runtime', workspace, payload: { content: error, independent: true },
|
|
889
|
+
}));
|
|
890
|
+
}
|
|
891
|
+
}
|
|
892
|
+
|
|
893
|
+
function skillInvocationErrorStatus(error) {
|
|
894
|
+
return ['skill_compile_failed', 'skill_arguments_invalid'].includes(error?.code) ? 400 : 422;
|
|
895
|
+
}
|
|
896
|
+
|
|
897
|
+
function skillResultErrorStatus(result) {
|
|
898
|
+
if (result?.code === 'skill_not_found') return 404;
|
|
899
|
+
if (result?.code === 'skill_arguments_invalid') return 400;
|
|
900
|
+
return 422;
|
|
901
|
+
}
|
|
902
|
+
|
|
903
|
+
function skillInvocationErrorMessage(error) {
|
|
904
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
905
|
+
return `Skill invocation could not be compiled: ${message}`;
|
|
668
906
|
}
|
|
669
907
|
|
|
670
908
|
function workspaceFromUrl(url) {
|
|
@@ -748,6 +986,37 @@ function controlQueueFor(session) {
|
|
|
748
986
|
return Array.isArray(session?.controlQueue) ? session.controlQueue : [];
|
|
749
987
|
}
|
|
750
988
|
|
|
989
|
+
function skillIdempotencyKey(workspace, turnId, skillName, args) {
|
|
990
|
+
const canonicalArguments = JSON.stringify(Object.fromEntries(
|
|
991
|
+
Object.entries(args ?? {}).sort(([left], [right]) => left.localeCompare(right)),
|
|
992
|
+
));
|
|
993
|
+
return createHash('sha256')
|
|
994
|
+
.update(`${workspace ?? ''}\0${turnId}\0${String(skillName).toLowerCase()}\0${canonicalArguments}`)
|
|
995
|
+
.digest('hex');
|
|
996
|
+
}
|
|
997
|
+
|
|
998
|
+
function publicSkillRunProjection(context, skillName, chainId, selectionKind, deduplicated) {
|
|
999
|
+
const chainItems = controlQueueFor(context?.session).filter((item) => item.chainId === chainId);
|
|
1000
|
+
const resolvedSelectionKind = selectionKind ?? chainItems.find((item) => item.selectionKind)?.selectionKind ?? null;
|
|
1001
|
+
const items = chainItems
|
|
1002
|
+
.sort((left, right) => Number(left.chainSequence ?? 0) - Number(right.chainSequence ?? 0))
|
|
1003
|
+
.map((item) => ({
|
|
1004
|
+
id: item.id,
|
|
1005
|
+
sequence: Number(item.chainSequence ?? 0),
|
|
1006
|
+
status: item.status,
|
|
1007
|
+
optional: item.optional === true,
|
|
1008
|
+
}));
|
|
1009
|
+
return {
|
|
1010
|
+
accepted: true,
|
|
1011
|
+
skill: skillName,
|
|
1012
|
+
chainId,
|
|
1013
|
+
objectiveCount: items.length,
|
|
1014
|
+
items,
|
|
1015
|
+
...(resolvedSelectionKind ? { selectionKind: resolvedSelectionKind } : {}),
|
|
1016
|
+
...(deduplicated ? { deduplicated: true } : {}),
|
|
1017
|
+
};
|
|
1018
|
+
}
|
|
1019
|
+
|
|
751
1020
|
function cancelQueuedControlItems(session, workspace = null) {
|
|
752
1021
|
const now = new Date().toISOString();
|
|
753
1022
|
const counted = new Set();
|
|
@@ -760,6 +1029,7 @@ function cancelQueuedControlItems(session, workspace = null) {
|
|
|
760
1029
|
item.finishedAt = now;
|
|
761
1030
|
item.updatedAt = now;
|
|
762
1031
|
const id = item.id ?? item.runId ?? `${item.input}:${item.createdAt}`;
|
|
1032
|
+
privateControlInputsFor(session).delete(item.id);
|
|
763
1033
|
if (!counted.has(id)) {
|
|
764
1034
|
counted.add(id);
|
|
765
1035
|
if (session) {
|
|
@@ -875,18 +1145,42 @@ async function handleControlMessage(context, store, input, { intent = null, star
|
|
|
875
1145
|
: controlMessage(context?.session, 'converse_while_idle'));
|
|
876
1146
|
}
|
|
877
1147
|
|
|
878
|
-
|
|
1148
|
+
/*
|
|
1149
|
+
`skillStack` accompagne l'élément, il ne vit pas sur la session.
|
|
1150
|
+
|
|
1151
|
+
La pile des compétences en cours était posée sur la session pour la durée d'UN
|
|
1152
|
+
run, et restaurée par son `finally`. Or une compétence imbriquée n'est pas
|
|
1153
|
+
exécutée en ligne : elle est MISE EN FILE, et son run démarre après que le
|
|
1154
|
+
parent a fini de se nettoyer. La pile qu'elle lisait était donc déjà vide.
|
|
1155
|
+
|
|
1156
|
+
Conséquence : la garde n'attrapait que le cas pour lequel elle avait été
|
|
1157
|
+
écrite — une compétence qui se relance dans son propre run — et laissait
|
|
1158
|
+
passer A→B→A, qui boucle jusqu'à épuisement du budget en headless.
|
|
1159
|
+
|
|
1160
|
+
Une file est un passage de témoin : ce qui doit survivre au parent voyage avec
|
|
1161
|
+
le message, pas dans l'état de celui qui l'a posté.
|
|
1162
|
+
*/
|
|
1163
|
+
function enqueueControlRequest(context, input, { publicInput = null, capabilityPlan, chainId, chainSequence, skillName, skillExecution, skillStack, selectionKind, optional = false, continueOnFailure = false } = {}) {
|
|
879
1164
|
const now = new Date().toISOString();
|
|
880
1165
|
const item = {
|
|
881
1166
|
id: `control-${randomUUID()}`,
|
|
882
1167
|
workspace: context?.workspace ?? context?.session?.workspace ?? null,
|
|
883
1168
|
type: 'run_request',
|
|
884
|
-
input,
|
|
1169
|
+
input: publicInput ?? input,
|
|
885
1170
|
status: 'queued',
|
|
886
1171
|
createdAt: now,
|
|
887
1172
|
updatedAt: now,
|
|
888
1173
|
...(capabilityPlan !== undefined ? { capabilityPlan } : {}),
|
|
1174
|
+
...(chainId ? { chainId } : {}),
|
|
1175
|
+
...(Number.isInteger(chainSequence) ? { chainSequence } : {}),
|
|
1176
|
+
...(skillName ? { skillName } : {}),
|
|
1177
|
+
...(skillExecution ? { skillExecution } : {}),
|
|
1178
|
+
...(Array.isArray(skillStack) && skillStack.length ? { skillStack: [...skillStack] } : {}),
|
|
1179
|
+
...(selectionKind ? { selectionKind } : {}),
|
|
1180
|
+
optional: optional === true,
|
|
1181
|
+
continueOnFailure: continueOnFailure === true,
|
|
889
1182
|
};
|
|
1183
|
+
privateControlInputsFor(context?.session).set(item.id, input);
|
|
890
1184
|
dispatchAgentEvent(context.session, createAgentEvent('control_enqueued', {
|
|
891
1185
|
origin: 'runtime',
|
|
892
1186
|
workspace: item.workspace,
|
|
@@ -895,6 +1189,23 @@ function enqueueControlRequest(context, input, { capabilityPlan } = {}) {
|
|
|
895
1189
|
return item;
|
|
896
1190
|
}
|
|
897
1191
|
|
|
1192
|
+
function emitControlSkipped(context, item, reason) {
|
|
1193
|
+
dispatchAgentEvent(context.session, createAgentEvent('control_skipped', {
|
|
1194
|
+
origin: 'runtime',
|
|
1195
|
+
workspace: item.workspace ?? context.workspace ?? null,
|
|
1196
|
+
payload: { id: item.id, reason, finishedAt: new Date().toISOString() },
|
|
1197
|
+
}));
|
|
1198
|
+
}
|
|
1199
|
+
|
|
1200
|
+
function emitControlCancelled(context, item, reason) {
|
|
1201
|
+
privateControlInputsFor(context?.session).delete(item.id);
|
|
1202
|
+
dispatchAgentEvent(context.session, createAgentEvent('control_cancelled', {
|
|
1203
|
+
origin: 'runtime',
|
|
1204
|
+
workspace: item.workspace ?? context.workspace ?? null,
|
|
1205
|
+
payload: { id: item.id, reason, finishedAt: new Date().toISOString() },
|
|
1206
|
+
}));
|
|
1207
|
+
}
|
|
1208
|
+
|
|
898
1209
|
function storeControlProposal(context, input, classification, status) {
|
|
899
1210
|
const now = new Date().toISOString();
|
|
900
1211
|
const patch = buildPlanPatchFromInput(input, status);
|