@dotdrelle/wiki-manager 0.15.28 → 0.15.32

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. package/.env.example +5 -2
  2. package/README.md +5 -4
  3. package/agents.docker-compose.override.example.yml +1 -1
  4. package/agents.docker-compose.yml +10 -0
  5. package/bin/wiki-manager.js +1 -1
  6. package/docker-compose.override.example.yml +3 -2
  7. package/mcp.endpoints.example.json +1 -1
  8. package/package.json +2 -2
  9. package/src/agent/graph.js +77 -13
  10. package/src/agent/graph.test.js +165 -8
  11. package/src/cli/runtimeStartup.test.js +14 -0
  12. package/src/cli/wiki-manager.js +118 -19
  13. package/src/cli/wiki-manager.test.js +192 -0
  14. package/src/commands/slash.js +168 -39
  15. package/src/commands/slash.test.js +100 -1
  16. package/src/core/agentsCompose.js +79 -0
  17. package/src/core/agentsCompose.test.js +99 -0
  18. package/src/core/buildInfo.json +2 -2
  19. package/src/core/composeOverrides.test.js +17 -3
  20. package/src/core/env.js +27 -10
  21. package/src/core/mcp.js +113 -38
  22. package/src/core/mcp.test.js +181 -20
  23. package/src/core/startupCheck.js +64 -27
  24. package/src/core/startupCheck.test.js +49 -1
  25. package/src/core/wikiSetup.js +57 -22
  26. package/src/core/wikiSetup.test.js +87 -0
  27. package/src/core/wikiWorkspace.test.js +16 -0
  28. package/src/orchestrator/agentRegistry.js +35 -7
  29. package/src/orchestrator/agentRegistry.test.js +73 -0
  30. package/src/orchestrator/dispatcher.js +6 -1
  31. package/src/orchestrator/dispatcher.test.js +24 -1
  32. package/src/orchestrator/objectiveResolver.js +24 -0
  33. package/src/orchestrator/objectiveResolver.test.js +27 -0
  34. package/src/runtime/auth.test.js +1 -65
  35. package/src/runtime/donna-contract.test.js +2 -1
  36. package/src/runtime/lifecycle.js +0 -33
  37. package/src/runtime/runner.test.js +10 -1
  38. package/src/runtime/supervisor.test.js +11 -1
  39. package/src/shell/LeftPane.tsx +49 -24
  40. package/src/shell/repl.js +114 -21
  41. package/src/shell/repl.test.js +88 -17
  42. package/src/shell/tui.tsx +10 -19
  43. package/wiki-workspace +98 -12
@@ -1,4 +1,5 @@
1
1
  import { randomUUID } from 'node:crypto';
2
+ import { spawnSync } from 'node:child_process';
2
3
  import { readFileSync } from 'node:fs';
3
4
  import { mkdir, writeFile } from 'node:fs/promises';
4
5
  import { dirname, join, resolve } from 'node:path';
@@ -25,6 +26,7 @@ import { listWorkspaces } from '../core/workspaces.js';
25
26
 
26
27
  const __dirname = dirname(fileURLToPath(import.meta.url));
27
28
  const packageJsonPath = resolve(__dirname, '../../package.json');
29
+ const workspaceCliPath = resolve(__dirname, '../../wiki-workspace');
28
30
  const packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf8'));
29
31
  const SHELL_COMMANDS = ['help', 'version', 'exit', 'workspace', 'new', 'use', 'config', 'status', 'services', 'start', 'stop', 'logs', 'mcp', 'connector', 'wiki', 'skills', 'clear', 'chat', 'agent', 'approve'];
30
32
 
@@ -106,7 +108,13 @@ export function buildExecutorOnlyFragment({ objective, workspace, selection }) {
106
108
  * then a JSON-text completion, then no arguments — the executor uses its own
107
109
  * defaults. It never throws and never invents identifiers.
108
110
  */
109
- export async function resolveExecutorArguments({ llm, objective, capability, signal } = {}) {
111
+ export async function resolveExecutorArguments({
112
+ llm,
113
+ objective,
114
+ capability,
115
+ workspace,
116
+ signal,
117
+ } = {}) {
110
118
  const schema = capability?.inputSchema;
111
119
  const objectiveText = String(objective ?? '').trim();
112
120
  if (
@@ -121,10 +129,22 @@ export async function resolveExecutorArguments({ llm, objective, capability, sig
121
129
  ) {
122
130
  return {};
123
131
  }
132
+ const workspaceName = String(workspace ?? '').trim();
124
133
  const system = [
125
134
  'Extract structured arguments for a task from the user objective.',
126
135
  'Only fill a field when the objective explicitly states or clearly implies its value.',
127
136
  'Omit every field that is not stated. Never invent identifiers, queries, filters or counts.',
137
+ // The objective almost always names the workspace ("export the pages of
138
+ // workspace acpi"), and the orchestrator already binds it out of band. With
139
+ // one free-text field in the schema and no field for the workspace, a model
140
+ // reliably misbinds the two — that is how a workspace name ended up as a
141
+ // source name and failed the task.
142
+ ...(workspaceName
143
+ ? [
144
+ `The task already runs against workspace "${workspaceName}"; the orchestrator supplies it separately.`,
145
+ `Never use "${workspaceName}" as the value of any field. If the objective only names the workspace, return {}.`,
146
+ ]
147
+ : []),
128
148
  'Return the arguments object only.',
129
149
  ].join('\n');
130
150
  const tool = {
@@ -150,9 +170,9 @@ export async function resolveExecutorArguments({ llm, objective, capability, sig
150
170
  });
151
171
  const call = (result?.tool_calls ?? []).find((item) => item?.function?.name === 'set_task_arguments');
152
172
  const fromCall = call ? safeParseArgumentObject(call.function?.arguments) : null;
153
- if (fromCall) return pruneArgumentsToSchema(fromCall, schema);
173
+ if (fromCall) return pruneArgumentsToSchema(fromCall, schema, workspaceName);
154
174
  const fromText = safeParseArgumentObject(result?.content);
155
- if (fromText) return pruneArgumentsToSchema(fromText, schema);
175
+ if (fromText) return pruneArgumentsToSchema(fromText, schema, workspaceName);
156
176
  } catch {
157
177
  // Fall through to the tool-less path.
158
178
  }
@@ -164,13 +184,48 @@ export async function resolveExecutorArguments({ llm, objective, capability, sig
164
184
  signal,
165
185
  });
166
186
  const fromText = safeParseArgumentObject(result?.content);
167
- if (fromText) return pruneArgumentsToSchema(fromText, schema);
187
+ if (fromText) return pruneArgumentsToSchema(fromText, schema, workspaceName);
168
188
  } catch {
169
189
  // Give up: the executor will use its own defaults.
170
190
  }
171
191
  return {};
172
192
  }
173
193
 
194
+ // The system prompt above is advice a model may ignore; this is not. A value
195
+ // that merely echoes the workspace name carries no information the orchestrator
196
+ // does not already hold, so dropping it can only widen the task to the
197
+ // executor's own default — never narrow it to something wrong. (A source
198
+ // genuinely named after its workspace degrades to "process everything", which
199
+ // still includes it.)
200
+ function echoesWorkspace(key, value, workspaceName) {
201
+ if (!workspaceName || typeof value !== 'string') return false;
202
+ if (/workspace/i.test(key)) return false;
203
+ return value.trim().toLowerCase() === String(workspaceName).trim().toLowerCase();
204
+ }
205
+
206
+ // A field with an `enum` declares a closed vocabulary, so an extracted value
207
+ // outside it is checkably wrong — not a judgement call about meaning. This is
208
+ // the only class of hallucinated identifier the orchestrator can reject
209
+ // without knowing anything about the business domain, which is exactly why
210
+ // agents must publish the vocabulary instead of a bare string.
211
+ function violatesEnum(schemaEntry, value) {
212
+ const values = schemaEntry?.enum;
213
+ if (!Array.isArray(values) || values.length === 0) return false;
214
+ return !values.some((allowed) => allowed === value);
215
+ }
216
+
217
+ export function missingRequiredArguments(schema, args) {
218
+ const required = Array.isArray(schema?.required) ? schema.required.map(String) : [];
219
+ const values = args && typeof args === 'object' && !Array.isArray(args) ? args : {};
220
+ return required.filter((key) => {
221
+ const value = values[key];
222
+ if (value === undefined || value === null) return true;
223
+ if (typeof value === 'string') return value.trim() === '';
224
+ if (Array.isArray(value)) return value.length === 0;
225
+ return false;
226
+ });
227
+ }
228
+
174
229
  function safeParseArgumentObject(text) {
175
230
  const cleaned = String(text ?? '').trim().replace(/^```(?:json)?\s*/i, '').replace(/\s*```$/, '');
176
231
  if (!cleaned) return null;
@@ -182,12 +237,14 @@ function safeParseArgumentObject(text) {
182
237
  }
183
238
  }
184
239
 
185
- function pruneArgumentsToSchema(value, schema) {
240
+ function pruneArgumentsToSchema(value, schema, workspaceName = '') {
186
241
  const allowed = schema.properties ?? {};
187
242
  const acceptsExtra = schema.additionalProperties !== false;
188
243
  const out = {};
189
244
  for (const [key, entry] of Object.entries(value)) {
190
245
  if (entry === undefined || entry === null) continue;
246
+ if (echoesWorkspace(key, entry, workspaceName)) continue;
247
+ if (violatesEnum(allowed[key], entry)) continue;
191
248
  if (Object.hasOwn(allowed, key) || acceptsExtra) out[key] = entry;
192
249
  }
193
250
  return out;
@@ -248,6 +305,12 @@ export function ensureInteractiveAssistantMessage(session, response, { turnId, w
248
305
  return true;
249
306
  }
250
307
 
308
+ export function mcpStatusNeedsRefresh(mcpStatus) {
309
+ return Object.values(mcpStatus ?? {}).some(
310
+ (endpoint) => endpoint?.url && endpoint.status !== 'connected',
311
+ );
312
+ }
313
+
251
314
  export async function forwardRuntimeApproval(getWorkspaceContext, request = {}) {
252
315
  const context = await getWorkspaceContext(request.workspace ?? null);
253
316
  return context.approvalManager?.approve(request) ?? { approved: false };
@@ -1011,8 +1074,16 @@ async function runRuntime(argv, agent) {
1011
1074
  llm: session.llm,
1012
1075
  objective,
1013
1076
  capability: provider.capability,
1077
+ workspace: session.workspace ?? context.workspace ?? '',
1014
1078
  signal: session._abortSignal,
1015
1079
  });
1080
+ const missingArguments = missingRequiredArguments(
1081
+ provider.capability?.inputSchema,
1082
+ extractedArguments,
1083
+ );
1084
+ if (missingArguments.length > 0) {
1085
+ throw new Error(`Delegation requires input: ${missingArguments.join(', ')}`);
1086
+ }
1016
1087
  fragment = buildExecutorOnlyFragment({
1017
1088
  objective,
1018
1089
  workspace: session.workspace ?? context.workspace ?? 'workspace',
@@ -1236,6 +1307,14 @@ async function runRuntime(argv, agent) {
1236
1307
  async function executeInteractiveTurn(context, body, { signal, turnId } = {}) {
1237
1308
  const input = String(body.input ?? body.prompt ?? '').trim();
1238
1309
  if (!input) throw new Error('Missing input.');
1310
+ // The runtime may start while optional agents are still stopped. `/start
1311
+ // agents` happens in the shell process, so its refreshed MCP snapshot does
1312
+ // not mutate this long-lived runtime context. Re-probe only while at least
1313
+ // one configured endpoint is disconnected; once connected, mcpRequest's
1314
+ // stale-session recovery handles later server restarts cheaply.
1315
+ if (mcpStatusNeedsRefresh(context.session.mcp)) {
1316
+ await refreshMcpRuntimeStatus(context.session);
1317
+ }
1239
1318
  const ephemeral = createInteractiveSession(context, { runtimeUrl: selfRuntimeUrl, turnId, signal });
1240
1319
  // Seed from a freshly reduced COPY of persisted events. Interactive turn
1241
1320
  // events deliberately do not mutate the canonical run projection, so the
@@ -1368,6 +1447,18 @@ function logImageRefreshErrors(imageRefresh) {
1368
1447
  }
1369
1448
 
1370
1449
  export async function runCli(argv) {
1450
+ if (argv.includes('--refresh')) {
1451
+ if (argv.length !== 1) throw new Error('--refresh does not accept other options.');
1452
+ const result = spawnSync(workspaceCliPath, ['refresh'], {
1453
+ cwd: process.cwd(),
1454
+ env: process.env,
1455
+ stdio: 'inherit',
1456
+ });
1457
+ if (result.error) throw result.error;
1458
+ if (result.status !== 0) throw new Error(`Refresh failed with exit code ${result.status ?? 1}.`);
1459
+ return;
1460
+ }
1461
+
1371
1462
  if (argv[0] === 'runtime') {
1372
1463
  const scaffolded = ensureManagerScaffold({ log: (message) => console.log(`[wiki-manager] ${message}`) });
1373
1464
  if (scaffolded.length > 0) loadManagerEnv();
@@ -1434,12 +1525,17 @@ export async function runCli(argv) {
1434
1525
  // in a random cwd must not litter files.
1435
1526
  const scaffolded = ensureManagerScaffold({ log: (message) => console.log(`[wiki-manager] ${message}`) });
1436
1527
  if (scaffolded.length > 0) loadManagerEnv();
1437
- const reportCheck = ({ kind, ok, detail, context, skipped, pending }) => {
1528
+ const reportCheck = ({ kind, ok, detail, context, skipped, pending, requested }) => {
1438
1529
  // "waiting" checks (containers not up yet, MCP not connected yet) are the
1439
1530
  // normal state at launch — the shell starts them and reports the real
1440
1531
  // status afterwards, so echoing them here is pure noise. Only what is
1441
1532
  // ready or actually broken is printed.
1442
- if (!ok && (pending || skipped)) return;
1533
+ //
1534
+ // `pending` alone cannot express this: a service the operator explicitly
1535
+ // asked for (`requested`) is still "pending" right after /start, and that
1536
+ // one MUST be shown — silence there is how a failed start reads as a
1537
+ // success. Boot silence applies to unrequested checks only.
1538
+ if (!ok && (pending || skipped) && !requested) return;
1443
1539
  const labels = { docker: 'Docker', internet: 'Internet', agents: 'Agent containers', workspace: 'Workspaces', containers: 'Workspace containers', mcp: 'MCP' };
1444
1540
  const label = labels[kind] ?? kind;
1445
1541
  const instruction = !ok && context?.command ? ` — command: ${context.command}` : '';
@@ -1452,10 +1548,12 @@ export async function runCli(argv) {
1452
1548
  console.log(`${color}${icon} configuration: ${label} ${state}${suffix}${instruction}\x1b[0m`);
1453
1549
  };
1454
1550
  let preflight = await runPreflightChecks({ onCheck: reportCheck });
1455
- if (preflight.gaps.length > 0) {
1456
- await runStartupWizard(preflight.gaps);
1457
- // The wizard may have created a workspace, started agents or repaired
1458
- // configuration. Re-read everything before drawing the home screen.
1551
+ const wizardGaps = startupWizardGaps(preflight.gaps);
1552
+ if (wizardGaps.length > 0) {
1553
+ await runStartupWizard(wizardGaps);
1554
+ // The wizard may have created a workspace or repaired configuration.
1555
+ // Agents are deliberately excluded: starting them is an explicit console
1556
+ // action through /start agents or /start all.
1459
1557
  preflight = await runPreflightChecks();
1460
1558
  }
1461
1559
  const dockerReady = preflight.checks.some((check) => check.kind === 'docker' && check.ok);
@@ -1480,10 +1578,8 @@ export async function runCli(argv) {
1480
1578
  console.error(`Runtime unavailable: ${runtime.error}`);
1481
1579
  }
1482
1580
  preflight = withRuntimePreflight(preflight, runtime);
1483
- // The owned-runtime shutdown happens inside the TUI's own exit paths
1484
- // (see tui.tsx onShellExit): render() resolves at MOUNT, so anything
1485
- // after this await would run while the shell is still on screen —
1486
- // 0.12.9 shipped exactly that bug and killed the runtime under the user.
1581
+ // render() resolves at mount; the TUI owns renderer teardown. The shared
1582
+ // runtime deliberately survives shell exit because `serve` may use it.
1487
1583
  await runOpenTuiShell({
1488
1584
  agent,
1489
1585
  packageJson,
@@ -1505,8 +1601,11 @@ export async function runCli(argv) {
1505
1601
  }
1506
1602
  }
1507
1603
  await runShell({ agent, packageJson, runtime });
1508
- if (runtime?.url) {
1509
- const { shutdownOwnedRuntime } = await import('../runtime/lifecycle.js');
1510
- await shutdownOwnedRuntime(runtime, { log: (message) => console.log(`[wiki-manager] ${message}`) });
1511
- }
1604
+ }
1605
+
1606
+ // Missing/stopped agents are status information, never a reason to interrupt
1607
+ // startup with a confirmation screen. The operator owns their lifecycle from
1608
+ // the console (`/start agents`, `/start all`, `/stop agents`).
1609
+ export function startupWizardGaps(gaps = []) {
1610
+ return gaps.filter((gap) => gap?.kind !== 'agents');
1512
1611
  }
@@ -3,8 +3,11 @@ import test from 'node:test';
3
3
  import {
4
4
  buildExecutorOnlyFragment,
5
5
  forwardRuntimeApproval,
6
+ mcpStatusNeedsRefresh,
7
+ missingRequiredArguments,
6
8
  resolveExecutorArguments,
7
9
  resolvePreparedDelegationApproval,
10
+ startupWizardGaps,
8
11
  } from './wiki-manager.js';
9
12
 
10
13
  const COLLECT_CAPABILITY = {
@@ -19,6 +22,32 @@ const COLLECT_CAPABILITY = {
19
22
  },
20
23
  };
21
24
 
25
+ test('startup never opens the setup wizard just because agents are stopped', () => {
26
+ const workspace = { kind: 'workspace', context: {} };
27
+ assert.deepEqual(
28
+ startupWizardGaps([
29
+ { kind: 'agents', context: { downServices: ['cme', 'documents'] } },
30
+ workspace,
31
+ ]),
32
+ [workspace],
33
+ );
34
+ assert.deepEqual(startupWizardGaps([{ kind: 'agents' }]), []);
35
+ });
36
+
37
+ test('interactive runtime refreshes configured MCP endpoints that started late', () => {
38
+ assert.equal(mcpStatusNeedsRefresh({
39
+ wiki: { url: 'http://127.0.0.1:3201/mcp', status: 'connected' },
40
+ cme: { url: 'http://127.0.0.1:3336/mcp/', status: 'configured' },
41
+ }), true);
42
+ assert.equal(mcpStatusNeedsRefresh({
43
+ wiki: { url: 'http://127.0.0.1:3201/mcp', status: 'connected' },
44
+ cme: { url: 'http://127.0.0.1:3336/mcp/', status: 'connected' },
45
+ }), false);
46
+ assert.equal(mcpStatusNeedsRefresh({
47
+ disabled: { url: null, status: 'missing' },
48
+ }), false);
49
+ });
50
+
22
51
  test('executor-only capabilities receive one manager-authored executable task', () => {
23
52
  const fragment = buildExecutorOnlyFragment({
24
53
  objective: 'donne-moi mes derniers mails',
@@ -96,6 +125,27 @@ test('argument extraction stays agnostic and safe when it cannot extract', async
96
125
  );
97
126
  });
98
127
 
128
+ test('required executor arguments become a conversational blocker before plan validation', () => {
129
+ const schema = {
130
+ type: 'object',
131
+ required: ['to', 'subject', 'body'],
132
+ properties: {
133
+ to: { type: 'string' },
134
+ subject: { type: 'string' },
135
+ body: { type: 'string' },
136
+ },
137
+ };
138
+ assert.deepEqual(missingRequiredArguments(schema, {}), ['to', 'subject', 'body']);
139
+ assert.deepEqual(
140
+ missingRequiredArguments(schema, { to: 'a@example.test', subject: 'Hello', body: 'Message' }),
141
+ [],
142
+ );
143
+ assert.deepEqual(
144
+ missingRequiredArguments(schema, { to: [], subject: ' ', body: 'Message' }),
145
+ ['to', 'subject'],
146
+ );
147
+ });
148
+
99
149
  test('runtime approval bridge preserves the complete run-scoped grant', async () => {
100
150
  let forwarded = null;
101
151
  const request = {
@@ -156,3 +206,145 @@ test('prepared delegation only approves when autoApprove is explicitly true', ()
156
206
  result: { approved: true },
157
207
  });
158
208
  });
209
+
210
+ const EXPORT_CAPABILITY = {
211
+ description: 'Export configured sources to workspace markdown files.',
212
+ inputSchema: {
213
+ type: 'object',
214
+ additionalProperties: true,
215
+ properties: { source_name: { type: 'string' } },
216
+ },
217
+ };
218
+
219
+ test('argument extraction drops a value that only echoes the active workspace', async () => {
220
+ // Regression: "exporter les pages Confluence du workspace acpi" against a
221
+ // schema whose single free-text field is source_name. The model binds the
222
+ // workspace name to it, and the executor fails with "source 'acpi' not
223
+ // found". The workspace is already bound out of band, so the echo is noise.
224
+ const llm = {
225
+ completeWithTools: async () => ({
226
+ tool_calls: [{
227
+ function: { name: 'set_task_arguments', arguments: JSON.stringify({ source_name: 'acpi' }) },
228
+ }],
229
+ }),
230
+ };
231
+
232
+ assert.deepEqual(
233
+ await resolveExecutorArguments({
234
+ llm,
235
+ objective: 'exporter les pages Confluence du workspace acpi',
236
+ capability: EXPORT_CAPABILITY,
237
+ workspace: 'acpi',
238
+ }),
239
+ {},
240
+ );
241
+ });
242
+
243
+ test('argument extraction keeps a real value that is not the workspace name', async () => {
244
+ const llm = {
245
+ completeWithTools: async () => ({
246
+ tool_calls: [{
247
+ function: {
248
+ name: 'set_task_arguments',
249
+ arguments: JSON.stringify({ source_name: 'EAS_Avant_projet_ACPI' }),
250
+ },
251
+ }],
252
+ }),
253
+ };
254
+
255
+ assert.deepEqual(
256
+ await resolveExecutorArguments({
257
+ llm,
258
+ objective: 'exporter la source EAS_Avant_projet_ACPI',
259
+ capability: EXPORT_CAPABILITY,
260
+ workspace: 'acpi',
261
+ }),
262
+ { source_name: 'EAS_Avant_projet_ACPI' },
263
+ );
264
+ });
265
+
266
+ test('argument extraction tells the model the workspace is already bound', async () => {
267
+ let seenSystem = '';
268
+ const llm = {
269
+ completeWithTools: async ({ system }) => {
270
+ seenSystem = system;
271
+ return { tool_calls: [] };
272
+ },
273
+ };
274
+
275
+ await resolveExecutorArguments({
276
+ llm,
277
+ objective: 'exporter les pages du workspace acpi',
278
+ capability: EXPORT_CAPABILITY,
279
+ workspace: 'acpi',
280
+ });
281
+
282
+ assert.match(seenSystem, /already runs against workspace "acpi"/);
283
+ });
284
+
285
+ test('argument extraction rejects a value outside the closed vocabulary', async () => {
286
+ // Regression: "exporter les pages Confluence du workspace juno" — the
287
+ // workspace guard removes "juno", so the model reaches for the next noun and
288
+ // emits "Confluence". Only the agent knows the valid names; once it publishes
289
+ // them as an enum, the orchestrator can check without guessing at meaning.
290
+ const capability = {
291
+ description: 'Export configured sources.',
292
+ inputSchema: {
293
+ type: 'object',
294
+ additionalProperties: true,
295
+ properties: {
296
+ source_name: { type: 'string', enum: ['EAS_Avant_projet_ACPI'] },
297
+ },
298
+ },
299
+ };
300
+ const llm = {
301
+ completeWithTools: async () => ({
302
+ tool_calls: [{
303
+ function: { name: 'set_task_arguments', arguments: JSON.stringify({ source_name: 'Confluence' }) },
304
+ }],
305
+ }),
306
+ };
307
+
308
+ assert.deepEqual(
309
+ await resolveExecutorArguments({
310
+ llm,
311
+ objective: 'exporter les pages Confluence du workspace juno',
312
+ capability,
313
+ workspace: 'juno',
314
+ }),
315
+ {},
316
+ );
317
+ });
318
+
319
+ test('argument extraction keeps a value the vocabulary allows', async () => {
320
+ const capability = {
321
+ description: 'Export configured sources.',
322
+ inputSchema: {
323
+ type: 'object',
324
+ additionalProperties: true,
325
+ properties: {
326
+ source_name: { type: 'string', enum: ['EAS_Avant_projet_ACPI', 'autre'] },
327
+ },
328
+ },
329
+ };
330
+ const llm = {
331
+ completeWithTools: async () => ({
332
+ tool_calls: [{
333
+ function: {
334
+ name: 'set_task_arguments',
335
+ arguments: JSON.stringify({ source_name: 'EAS_Avant_projet_ACPI' }),
336
+ },
337
+ }],
338
+ }),
339
+ };
340
+
341
+ assert.deepEqual(
342
+ await resolveExecutorArguments({
343
+ llm,
344
+ objective: 'exporter la source EAS_Avant_projet_ACPI',
345
+ capability,
346
+ workspace: 'acpi',
347
+ }),
348
+ { source_name: 'EAS_Avant_projet_ACPI' },
349
+ );
350
+ });