maka-agent 0.2.0-dev.76.20261009 → 0.2.0-dev.78.20261010

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 (78) hide show
  1. package/README.md +1 -1
  2. package/README.zh-CN.md +3 -3
  3. package/dist/pi-tui-pickers.js +5 -2
  4. package/dist/pi-tui-runner.js +211 -33
  5. package/dist/runtime-host-session-driver.js +91 -13
  6. package/dist/session-catalog-limits.js +22 -0
  7. package/dist/session-driver.js +10 -0
  8. package/dist/tui-copy-catalog.js +15 -0
  9. package/native/runtime-host-windows-task-launcher/prebuilds/win32-x64/maka-runtime-host-task-launcher.exe +0 -0
  10. package/node_modules/@maka/core/dist/bot-chat-settings.js +3 -0
  11. package/node_modules/@maka/core/dist/bot-events.js +1 -8
  12. package/node_modules/@maka/core/dist/local-memory.js +38 -1
  13. package/node_modules/@maka/core/dist/scheduled-task.js +14 -15
  14. package/node_modules/@maka/core/dist/session.js +3 -0
  15. package/node_modules/@maka/core/dist/text-sanitize.js +8 -3
  16. package/node_modules/@maka/runtime/dist/agent-graph-supervisor-wake.js +115 -17
  17. package/node_modules/@maka/runtime/dist/agent-run.js +11 -0
  18. package/node_modules/@maka/runtime/dist/ai-sdk-backend.js +11 -0
  19. package/node_modules/@maka/runtime/dist/ai-sdk-turn.js +13 -5
  20. package/node_modules/@maka/runtime/dist/bots/proxied-fetch.js +29 -1
  21. package/node_modules/@maka/runtime/dist/computer-use-observation-text.js +22 -0
  22. package/node_modules/@maka/runtime/dist/conversation-copy-agent-output.js +227 -0
  23. package/node_modules/@maka/runtime/dist/conversation-copy.js +176 -46
  24. package/node_modules/@maka/runtime/dist/file-stable-write.js +15 -3
  25. package/node_modules/@maka/runtime/dist/filesystem-worker/resource-resolver.js +3 -1
  26. package/node_modules/@maka/runtime/dist/model-adapter.js +20 -1
  27. package/node_modules/@maka/runtime/dist/network/proxy-test.js +28 -1
  28. package/node_modules/@maka/runtime/dist/network/scoped-fetch-transport.js +13 -2
  29. package/node_modules/@maka/runtime/dist/owned-child-process.js +209 -0
  30. package/node_modules/@maka/runtime/dist/owned-process-main.js +183 -0
  31. package/node_modules/@maka/runtime/dist/pipe-process-driver.js +19 -25
  32. package/node_modules/@maka/runtime/dist/runtime-kernel.js +216 -8
  33. package/node_modules/@maka/runtime/dist/sandbox-boundary-tool.js +12 -0
  34. package/node_modules/@maka/runtime/dist/session-manager.js +156 -17
  35. package/node_modules/@maka/runtime/dist/shell-exec.js +18 -20
  36. package/node_modules/@maka/runtime/dist/shell-run-manager.js +2 -1
  37. package/node_modules/@maka/runtime/dist/stream-graph-coordinator.js +279 -8
  38. package/node_modules/@maka/runtime/dist/stream-graph-schedule-reconcile.js +209 -37
  39. package/node_modules/@maka/runtime/dist/tool-availability.js +55 -18
  40. package/node_modules/@maka/runtime/dist/tool-call-snapshot.js +142 -0
  41. package/node_modules/@maka/runtime/dist/tool-runtime.js +27 -84
  42. package/node_modules/@maka/runtime/dist/workers/{filesystem-worker.js → filesystem-worker.mjs} +12 -1
  43. package/node_modules/@maka/runtime-host/dist/client/connect-or-spawn.js +30 -6
  44. package/node_modules/@maka/runtime-host/dist/client/index.js +1 -1
  45. package/node_modules/@maka/runtime-host/dist/client/reconnect-lifecycle.js +44 -13
  46. package/node_modules/@maka/runtime-host/dist/client/session-catalog-summary.js +6 -0
  47. package/node_modules/@maka/runtime-host/dist/client/wsl-environment.js +4 -0
  48. package/node_modules/@maka/runtime-host/dist/protocol/index.js +5 -1
  49. package/node_modules/@maka/runtime-host/dist/protocol/message.js +1 -13
  50. package/node_modules/@maka/runtime-host/dist/protocol/session-catalog.js +46 -9
  51. package/node_modules/@maka/runtime-host/dist/protocol/session-continuity.js +1 -13
  52. package/node_modules/@maka/runtime-host/dist/protocol/turn.js +1 -13
  53. package/node_modules/@maka/runtime-host/dist/protocol/workhub-coordination.js +4 -7
  54. package/node_modules/@maka/runtime-host/dist/server/archive-retention-coordinator.js +81 -105
  55. package/node_modules/@maka/runtime-host/dist/server/candidate.js +47 -40
  56. package/node_modules/@maka/runtime-host/dist/server/execution-composition.js +87 -67
  57. package/node_modules/@maka/runtime-host/dist/server/execution-service.js +5 -1
  58. package/node_modules/@maka/runtime-host/dist/server/host-maintenance-guards.js +95 -0
  59. package/node_modules/@maka/runtime-host/dist/server/hosted-execution-tool-profile.js +18 -8
  60. package/node_modules/@maka/runtime-host/dist/server/interactive-run-composer.js +32 -3
  61. package/node_modules/@maka/runtime-host/dist/server/project-catalog-coordinator.js +2 -0
  62. package/node_modules/@maka/runtime-host/dist/server/root-turn-coordinator.js +109 -8
  63. package/node_modules/@maka/runtime-host/dist/server/scheduled-task-coordinator.js +20 -3
  64. package/node_modules/@maka/runtime-host/dist/server/session-background-activity.js +71 -0
  65. package/node_modules/@maka/runtime-host/dist/server/session-catalog-coordinator.js +8 -4
  66. package/node_modules/@maka/runtime-host/dist/server/session-interaction-activity.js +78 -0
  67. package/node_modules/@maka/runtime-host/dist/server/session-retirement-coordinator.js +56 -0
  68. package/node_modules/@maka/runtime-host/dist/server/session-revision-graph-references.js +16 -2
  69. package/node_modules/@maka/runtime-host/dist/server/skill-catalog-coordinator.js +2 -0
  70. package/node_modules/@maka/runtime-host/dist/server/workhub-coordination-action-gate.js +19 -24
  71. package/node_modules/@maka/runtime-host/dist/server/workhub-coordination-coordinator.js +74 -19
  72. package/node_modules/@maka/runtime-host/dist/server/workhub-result-coordinator.js +1 -1
  73. package/node_modules/@maka/runtime-host/dist/server/workhub-result-runtime.js +6 -99
  74. package/node_modules/@maka/runtime-host/dist/server/workhub-target-execution-authority.js +14 -126
  75. package/node_modules/@maka/storage/dist/context-offload-store.js +8 -3
  76. package/node_modules/@maka/storage/dist/git-entry.js +142 -5
  77. package/node_modules/@maka/storage/dist/session-bundle-policy.js +54 -19
  78. package/package.json +2 -2
@@ -22,6 +22,15 @@ import { projectAgentGraphSchedule, } from './stream-graph-supervisor-tools.js';
22
22
  import { stableHash } from './request-shape.js';
23
23
  const MAX_RECONCILIATION_ATTEMPTS = 8;
24
24
  const SCHEDULE_INTENT_SCHEMA_VERSION = 1;
25
+ // A failed control pass can park the wave it is stopping, and nothing else may
26
+ // wake it. Retry with a capped backoff while that wave is still running.
27
+ const CONTROL_RETRY_INITIAL_DELAY_MS = 100;
28
+ const CONTROL_RETRY_MAX_DELAY_MS = 5_000;
29
+ // The eighth consecutive failure of a stop or a control read lands about 11 s
30
+ // after the first, once the backoff has reached its cap: a fault that clears
31
+ // during the ramp is never reported as stuck, while a persistent one is
32
+ // surfaced instead of leaving the graph parked behind a single failure record.
33
+ const CONTROL_STUCK_FAILURES = 8;
25
34
  /**
26
35
  * Applies durable supervisor schedule intent to existing and newly
27
36
  * materialized graph operators.
@@ -35,6 +44,39 @@ export async function reconcileAgentGraphSchedule(input) {
35
44
  if (!Number.isSafeInteger(input.maxNewActivations) || input.maxNewActivations < 0) {
36
45
  throw new Error('Agent graph maxNewActivations must be a non-negative safe integer');
37
46
  }
47
+ const wake = createReconciliationWake();
48
+ // Install before reading the durable schedule: a commit during any await
49
+ // remains pending until this reconciliation consumes it.
50
+ const unsubscribe = input.subscribeToScheduleChanges?.(wake.notify);
51
+ try {
52
+ return await reconcileSchedule(input, wake);
53
+ }
54
+ finally {
55
+ unsubscribe?.();
56
+ }
57
+ }
58
+ function createReconciliationWake() {
59
+ let pending = false;
60
+ let resolve;
61
+ let next = new Promise((done) => {
62
+ resolve = done;
63
+ });
64
+ return {
65
+ notify() {
66
+ pending = true;
67
+ resolve();
68
+ },
69
+ async wait() {
70
+ if (!pending)
71
+ await next;
72
+ pending = false;
73
+ next = new Promise((done) => {
74
+ resolve = done;
75
+ });
76
+ },
77
+ };
78
+ }
79
+ async function reconcileSchedule(input, wake) {
38
80
  const processedIntentIds = new Set();
39
81
  const dispatches = [];
40
82
  const stops = [];
@@ -43,6 +85,11 @@ export async function reconcileAgentGraphSchedule(input) {
43
85
  // reconciliation; cache them per source graph so repeated snapshot reads do
44
86
  // not replay the full committed projection of a closed epoch.
45
87
  const selectedResultCache = new Map();
88
+ // Exact stops that already succeeded in this reconciliation; later passes
89
+ // and wakes do not stop the same activation again.
90
+ const resolvedStops = new Set();
91
+ // Consecutive stop failures per target, reset when the target resolves.
92
+ const stopFailureStreaks = new Map();
46
93
  let newActivationCount = 0;
47
94
  let observedExistingActivationCount = 0;
48
95
  let snapshot = await readScheduleSnapshot(input, selectedResultCache);
@@ -50,10 +97,8 @@ export async function reconcileAgentGraphSchedule(input) {
50
97
  if (input.abortSignal?.aborted) {
51
98
  return reconciliationResult('cancelled', newActivationCount, observedExistingActivationCount, dispatches, stops, [], failures, snapshot);
52
99
  }
53
- const stopWave = await applyScheduleStops(input, snapshot);
54
- stops.push(...stopWave.stops);
55
- for (const failure of stopWave.failures)
56
- recordReconciliationFailure(input, failures, failure);
100
+ const stopWave = await applyScheduleStops(input, snapshot, resolvedStops);
101
+ recordScheduleStops(input, stops, failures, stopFailureStreaks, stopWave);
57
102
  if (failures.length > 0) {
58
103
  snapshot = await readScheduleSnapshot(input, selectedResultCache);
59
104
  return reconciliationResult(input.abortSignal?.aborted ? 'cancelled' : 'failed', newActivationCount, observedExistingActivationCount, dispatches, stops, [], failures, snapshot);
@@ -196,13 +241,81 @@ export async function reconcileAgentGraphSchedule(input) {
196
241
  snapshot = await readScheduleSnapshot(input, selectedResultCache);
197
242
  return reconciliationResult('failed', newActivationCount, observedExistingActivationCount, dispatches, stops, deferredWork, failures, snapshot);
198
243
  }
199
- const outcomes = await Promise.all(prepared.map(async (work) => {
244
+ let waveSettled = false;
245
+ const wave = Promise.all(prepared.map(async (work) => {
200
246
  const outcome = await dispatchScheduledWork(input, work, snapshot.schedule.revision);
201
247
  if (outcome.status === 'rejected') {
202
248
  notifySupervisor(input.supervisor?.onReconciliationFailure, outcome.failure);
203
249
  }
204
250
  return outcome;
205
- }));
251
+ })).finally(() => {
252
+ waveSettled = true;
253
+ wake.notify();
254
+ });
255
+ let controlReadFailure;
256
+ let controlReadFailureStreak = 0;
257
+ let retryTimer;
258
+ let retryDelay = CONTROL_RETRY_INITIAL_DELAY_MS;
259
+ try {
260
+ // A driver abort does not end this loop: the wave still owns its
261
+ // dispatches, and a child parked behind a failed stop settles only
262
+ // after a retry succeeds.
263
+ while (!waveSettled) {
264
+ await wake.wait();
265
+ clearTimeout(retryTimer);
266
+ retryTimer = undefined;
267
+ if (waveSettled)
268
+ break;
269
+ let retry;
270
+ try {
271
+ const latest = await readScheduleSnapshot(input, selectedResultCache);
272
+ const controls = await applyScheduleStops(input, latest, resolvedStops);
273
+ recordScheduleStops(input, stops, failures, stopFailureStreaks, controls);
274
+ if (controlReadFailure) {
275
+ failures.splice(failures.indexOf(controlReadFailure), 1);
276
+ controlReadFailure = undefined;
277
+ }
278
+ controlReadFailureStreak = 0;
279
+ retry = controls.failures.length > 0;
280
+ }
281
+ catch (error) {
282
+ if (controlReadFailure)
283
+ failures.splice(failures.indexOf(controlReadFailure), 1);
284
+ controlReadFailureStreak += 1;
285
+ // Like a stop, a read failure notifies on its first occurrence and,
286
+ // once, when it has persisted long enough to be stuck.
287
+ controlReadFailure =
288
+ controlReadFailureStreak < CONTROL_STUCK_FAILURES
289
+ ? { phase: 'schedule', error }
290
+ : {
291
+ phase: 'schedule',
292
+ error: new Error(`Reading graph schedule controls is stuck after ${controlReadFailureStreak} consecutive failures and is still retrying`, { cause: error }),
293
+ };
294
+ failures.push(controlReadFailure);
295
+ if (controlReadFailureStreak === 1 ||
296
+ controlReadFailureStreak === CONTROL_STUCK_FAILURES) {
297
+ notifySupervisor(input.supervisor?.onReconciliationFailure, controlReadFailure);
298
+ }
299
+ retry = true;
300
+ }
301
+ // A failed stop can keep its child, and so this wave, parked until a
302
+ // retry succeeds. Back off instead of waiting for another commit.
303
+ if (retry) {
304
+ retryTimer = setTimeout(wake.notify, retryDelay);
305
+ retryDelay = Math.min(retryDelay * 2, CONTROL_RETRY_MAX_DELAY_MS);
306
+ }
307
+ else {
308
+ retryDelay = CONTROL_RETRY_INITIAL_DELAY_MS;
309
+ }
310
+ }
311
+ }
312
+ finally {
313
+ clearTimeout(retryTimer);
314
+ // This wave remains the sole owner of all dispatched executions, even
315
+ // if a control-store read fails while processing a schedule wake.
316
+ await wave;
317
+ }
318
+ const outcomes = await wave;
206
319
  let stale = false;
207
320
  for (const outcome of outcomes) {
208
321
  if (outcome.status === 'stale') {
@@ -259,6 +372,7 @@ async function readScheduleSnapshot(input, selectedResultCache) {
259
372
  assertGraphObservation(input.topology.graphId, observation);
260
373
  notifySupervisor(input.supervisor?.onObservation, observation);
261
374
  const schedule = projectAgentGraphSchedule(input.topology.graphId, updates);
375
+ notifySupervisor(input.onScheduleObserved, schedule);
262
376
  const selectedInputs = schedule.work
263
377
  .filter((work) => work.status === 'requested')
264
378
  .flatMap((work) => work.selectedResultInputs ?? []);
@@ -359,7 +473,43 @@ async function resolveSelectedResultRecords(input, selectedInputs, cache) {
359
473
  function selectedResultKey(selected) {
360
474
  return `${selected.sourceGraphId}\u0000${selected.resultId}`;
361
475
  }
362
- async function applyScheduleStops(input, snapshot) {
476
+ function recordScheduleStops(input, stops, failures, streaks, controls) {
477
+ stops.push(...controls.stops);
478
+ const resolvedTargets = new Set(controls.stops.map((stop) => stop.targetId));
479
+ for (const targetId of resolvedTargets)
480
+ streaks.delete(targetId);
481
+ for (let index = failures.length - 1; index >= 0; index -= 1) {
482
+ const failure = failures[index];
483
+ if (failure.phase === 'stop' && failure.targetId && resolvedTargets.has(failure.targetId)) {
484
+ failures.splice(index, 1);
485
+ }
486
+ }
487
+ for (const failure of controls.failures) {
488
+ const targetId = failure.targetId ?? '';
489
+ const streak = (streaks.get(targetId) ?? 0) + 1;
490
+ streaks.set(targetId, streak);
491
+ // The stuck state travels in the message: supervisor notifications are
492
+ // structured clones and the durable client failure keeps only the reason.
493
+ const recorded = streak < CONTROL_STUCK_FAILURES
494
+ ? failure
495
+ : {
496
+ ...failure,
497
+ error: new Error(`Stop for graph target ${targetId} is stuck after ${streak} consecutive failures and is still retrying`, { cause: failure.error }),
498
+ };
499
+ // A retried stop that fails again replaces its earlier failure. The
500
+ // supervisor hears about the first failure and, once, about a stuck stop,
501
+ // not about every backoff attempt.
502
+ const previous = failures.findIndex((existing) => existing.phase === 'stop' && existing.targetId === failure.targetId);
503
+ if (previous >= 0)
504
+ failures[previous] = recorded;
505
+ else
506
+ failures.push(recorded);
507
+ if (streak === 1 || streak === CONTROL_STUCK_FAILURES) {
508
+ notifySupervisor(input.supervisor?.onReconciliationFailure, recorded);
509
+ }
510
+ }
511
+ }
512
+ async function applyScheduleStops(input, snapshot, resolvedStops) {
363
513
  const requests = new Map();
364
514
  for (const stopped of snapshot.schedule.stoppedTargets) {
365
515
  requests.set(stopped.targetId, stopped.reason);
@@ -375,7 +525,7 @@ async function applyScheduleStops(input, snapshot) {
375
525
  const claimsByIntent = new Map(snapshot.claims.map((claim) => [claim.intentId, claim]));
376
526
  const activationTargets = graphActivationTargets(snapshot.observation);
377
527
  const immediate = [];
378
- const targetsBySession = new Map();
528
+ const targets = [];
379
529
  const failures = [];
380
530
  for (const [targetId, reason] of requests) {
381
531
  const work = workById.get(targetId);
@@ -386,13 +536,26 @@ async function applyScheduleStops(input, snapshot) {
386
536
  ? activationTargets.get(claim.targetRunId)
387
537
  : activationTargets.get(targetId);
388
538
  if (activation && isTerminalActivationStatus(activation.status)) {
389
- immediate.push({
390
- targetId,
391
- reason,
392
- status: 'already_terminal',
539
+ const identity = {
393
540
  sessionId: activation.sessionId,
394
- activationId: activation.activationId,
395
- });
541
+ runId: activation.activationId,
542
+ turnId: activation.turnId,
543
+ };
544
+ // A durable terminal does not prove that backend cleanup succeeded.
545
+ // Retry the exact stop only while a failed stop retains cleanup for this
546
+ // Run; a settled historical target resolves without a runtime call.
547
+ if (resolvedStops.has(stopTargetKey(targetId, identity)) ||
548
+ !input.stopController.hasPendingAgentGraphActivationStop?.(identity)) {
549
+ immediate.push({
550
+ targetId,
551
+ reason,
552
+ status: 'already_terminal',
553
+ sessionId: activation.sessionId,
554
+ activationId: activation.activationId,
555
+ });
556
+ continue;
557
+ }
558
+ targets.push({ targetId, reason, status: 'already_terminal', identity });
396
559
  continue;
397
560
  }
398
561
  if (work && claim) {
@@ -424,36 +587,37 @@ async function applyScheduleStops(input, snapshot) {
424
587
  }
425
588
  const sessionId = activation?.sessionId ?? claim.targetSessionId;
426
589
  const activationId = activation?.activationId ?? claim.targetRunId;
427
- const sessionTargets = targetsBySession.get(sessionId) ?? [];
428
- sessionTargets.push({
429
- targetId,
430
- reason,
431
- activationId,
432
- });
433
- targetsBySession.set(sessionId, sessionTargets);
590
+ const turnId = activation?.turnId ?? claim.targetTurnId;
591
+ const identity = { sessionId, runId: activationId, turnId };
592
+ if (resolvedStops.has(stopTargetKey(targetId, identity))) {
593
+ immediate.push({ targetId, reason, status: 'stopped', sessionId, activationId });
594
+ continue;
595
+ }
596
+ targets.push({ targetId, reason, status: 'stopped', identity });
434
597
  }
435
- const settled = await Promise.allSettled([...targetsBySession].map(async ([sessionId, targets]) => {
436
- await input.stopController.stopSession(sessionId, { source: 'graph_supervisor' });
437
- return targets.map((target) => ({
598
+ const settled = await Promise.allSettled(targets.map(async (target) => {
599
+ await input.stopController.stopAgentGraphActivation(target.identity, {
600
+ source: 'graph_supervisor',
601
+ });
602
+ return {
438
603
  targetId: target.targetId,
439
604
  reason: target.reason,
440
- status: 'stopped',
441
- sessionId,
442
- activationId: target.activationId,
443
- }));
605
+ status: target.status,
606
+ sessionId: target.identity.sessionId,
607
+ activationId: target.identity.runId,
608
+ };
444
609
  }));
445
610
  settled.forEach((result, index) => {
611
+ const target = targets[index];
446
612
  if (result.status === 'fulfilled') {
447
- immediate.push(...result.value);
613
+ resolvedStops.add(stopTargetKey(target.targetId, target.identity));
614
+ immediate.push(result.value);
448
615
  return;
449
616
  }
450
- const [sessionId, targets] = [...targetsBySession][index];
451
- targets.forEach((target) => {
452
- failures.push({
453
- phase: 'stop',
454
- targetId: target.targetId,
455
- error: new Error(`Failed to stop graph supervisor target ${target.targetId} in ${sessionId}`, { cause: result.reason }),
456
- });
617
+ failures.push({
618
+ phase: 'stop',
619
+ targetId: target.targetId,
620
+ error: new Error(`Failed to stop graph supervisor target ${target.targetId} in ${target.identity.sessionId}`, { cause: result.reason }),
457
621
  });
458
622
  });
459
623
  return {
@@ -461,6 +625,9 @@ async function applyScheduleStops(input, snapshot) {
461
625
  failures,
462
626
  };
463
627
  }
628
+ function stopTargetKey(targetId, identity) {
629
+ return [targetId, identity.sessionId, identity.runId, identity.turnId].join('\u0000');
630
+ }
464
631
  async function dispatchScheduledWork(input, prepared, expectedRevision) {
465
632
  let admission;
466
633
  try {
@@ -593,7 +760,7 @@ function scheduledWorkIntent(topology, observation, work, provision) {
593
760
  ],
594
761
  };
595
762
  }
596
- function scheduledWorkIntentId(graphId, workId) {
763
+ export function scheduledWorkIntentId(graphId, workId) {
597
764
  const hash = stableHash({
598
765
  schemaVersion: SCHEDULE_INTENT_SCHEMA_VERSION,
599
766
  graphId,
@@ -712,15 +879,20 @@ function orderedRequestedWork(schedule) {
712
879
  }
713
880
  function graphActivationTargets(observation) {
714
881
  const targets = new Map();
882
+ const sourceByActivation = new Map(observation.projection.records.map((record) => [record.activationId, record.source]));
715
883
  for (const binding of observation.projection.operators) {
716
884
  const state = observation.projection.state.operators[binding.operatorId];
717
885
  for (const activation of Object.values(state?.activations ?? {})) {
718
886
  if (targets.has(activation.activationId)) {
719
887
  throw new Error(`Graph activation ${activation.activationId} belongs to multiple operators`);
720
888
  }
889
+ const source = sourceByActivation.get(activation.activationId);
890
+ if (!source)
891
+ throw new Error(`Graph activation ${activation.activationId} has no source`);
721
892
  targets.set(activation.activationId, {
722
893
  sessionId: binding.sessionId,
723
894
  activationId: activation.activationId,
895
+ turnId: source.turnId,
724
896
  status: activation.status,
725
897
  });
726
898
  }
@@ -106,9 +106,9 @@ export function toolAvailabilityConnectorNames(tools, config) {
106
106
  /**
107
107
  * Immutable, backend-scoped bound-tool inventory and MiniSearch index.
108
108
  *
109
- * Mutable activation belongs to the per-send TurnScope and is passed to
110
- * prepare(). Constructing one AiSdkBackend therefore constructs one index; all
111
- * turns on that backend reuse it without sharing activation state.
109
+ * Mutable activation is passed into prepare(). One AiSdkBackend constructs one
110
+ * index; Turns on that backend share the Session activation map so later
111
+ * Turns keep previously activated schemas.
112
112
  */
113
113
  export class ToolAvailabilityRuntime {
114
114
  invalidTool;
@@ -249,7 +249,11 @@ export class ToolAvailabilityRuntime {
249
249
  diagnostics: () => undefined,
250
250
  };
251
251
  }
252
- const connector = this.buildSearchConnector(activeTools);
252
+ const requiredNames = [...requiredToolNames].filter((name) => this.toolsByName.has(name));
253
+ const requiredNameSet = new Set(requiredNames);
254
+ // Required names are already on the provider list. Omit them from the
255
+ // search inventory so an exact-name search does not "activate" them again.
256
+ const connector = this.buildSearchConnector(activeTools, requiredNameSet);
253
257
  const allTools = [...this.tools, connector];
254
258
  const canonical = canonicalizeToolSet(allTools, this.invalidTool);
255
259
  const knownNames = new Set(canonical.providerTools.map((tool) => tool.name));
@@ -260,7 +264,6 @@ export class ToolAvailabilityRuntime {
260
264
  if (this.activationKeysByName.get(name) !== activatedKey)
261
265
  activeTools.delete(name);
262
266
  }
263
- const requiredNames = [...requiredToolNames].filter((name) => knownNames.has(name));
264
267
  const step = { active: new Set() };
265
268
  const computeActive = () => {
266
269
  const names = new Set([...this.directNames, TOOL_SEARCH_NAME]);
@@ -284,12 +287,16 @@ export class ToolAvailabilityRuntime {
284
287
  diagnostics: (active, chars) => this.buildDiagnostic(allTools, active, chars),
285
288
  };
286
289
  }
287
- buildSearchConnector(activeTools) {
290
+ buildSearchConnector(activeTools, requiredToolNames) {
288
291
  return {
289
292
  name: TOOL_SEARCH_NAME,
290
- description: renderInventory(this.groups),
293
+ description: renderInventory(this.groups, requiredToolNames),
291
294
  parameters: z.object({
292
- query: z.string().trim().min(1).describe('Search query describing the needed capability.'),
295
+ query: z
296
+ .string()
297
+ .trim()
298
+ .min(1)
299
+ .describe('Search query describing the needed capability. An exact catalog tool name activates only that tool.'),
293
300
  limit: z
294
301
  .number()
295
302
  .int()
@@ -300,11 +307,7 @@ export class ToolAvailabilityRuntime {
300
307
  }),
301
308
  impl: ({ query, limit = TOOL_SEARCH_DEFAULT_LIMIT }, context) => {
302
309
  const normalizedQuery = query.trim();
303
- const ranked = this.searchIndex.search(normalizedQuery)
304
- .map((result) => String(result.id))
305
- .filter((name) => !activeTools.has(name))
306
- .slice(0, TOOL_SEARCH_MAX_LIMIT)
307
- .filter((name) => this.searchableNames.has(name));
310
+ const ranked = this.rankDeferredSearchHits(normalizedQuery, activeTools, requiredToolNames);
308
311
  const activated = [];
309
312
  let blocked;
310
313
  let schemaChars = 0;
@@ -355,6 +358,38 @@ export class ToolAvailabilityRuntime {
355
358
  },
356
359
  };
357
360
  }
361
+ /**
362
+ * Rank deferred tools for one search. An exact catalog name (case-sensitive,
363
+ * then unique case-insensitive) never expands into MiniSearch neighbors, so
364
+ * naming one tool cannot invalidate the prompt-cache prefix with up to
365
+ * `TOOL_SEARCH_DEFAULT_LIMIT` extra schemas.
366
+ */
367
+ rankDeferredSearchHits(query, activeTools, requiredToolNames) {
368
+ const withheld = (name) => activeTools.has(name) || requiredToolNames.has(name);
369
+ const exactName = this.resolveExactCatalogName(query);
370
+ if (exactName !== undefined) {
371
+ return this.searchableNames.has(exactName) && !withheld(exactName) ? [exactName] : [];
372
+ }
373
+ return this.searchIndex.search(query)
374
+ .map((result) => String(result.id))
375
+ .filter((name) => !withheld(name))
376
+ .slice(0, TOOL_SEARCH_MAX_LIMIT)
377
+ .filter((name) => this.searchableNames.has(name));
378
+ }
379
+ resolveExactCatalogName(query) {
380
+ if (this.toolsByName.has(query))
381
+ return query;
382
+ const lowered = query.toLowerCase();
383
+ let match;
384
+ for (const name of this.toolsByName.keys()) {
385
+ if (name.toLowerCase() !== lowered)
386
+ continue;
387
+ if (match !== undefined)
388
+ return undefined;
389
+ match = name;
390
+ }
391
+ return match;
392
+ }
358
393
  buildDiagnostic(allTools, active, visibleToolSchemaChars) {
359
394
  const activeSet = new Set(active);
360
395
  const enabledSourceIds = this.groups
@@ -384,11 +419,13 @@ export class ToolAvailabilityRuntime {
384
419
  };
385
420
  }
386
421
  }
387
- function renderInventory(groups) {
388
- const lines = groups.flatMap((group) => [
389
- `${group.id}:`,
390
- ...group.toolNames.map((name) => `- ${name}`),
391
- ]);
422
+ function renderInventory(groups, omitNames = new Set()) {
423
+ const lines = groups.flatMap((group) => {
424
+ const names = group.toolNames.filter((name) => !omitNames.has(name));
425
+ if (names.length === 0)
426
+ return [];
427
+ return [`${group.id}:`, ...names.map((name) => `- ${name}`)];
428
+ });
392
429
  return [
393
430
  'Search the deferred tools bound to this run. A successful search activates the',
394
431
  'bounded top matches; their complete callable definitions become visible on the',
@@ -0,0 +1,142 @@
1
+ /*
2
+ * Licensed to the Apache Software Foundation (ASF) under one
3
+ * or more contributor license agreements. See the NOTICE file
4
+ * distributed with this work for additional information
5
+ * regarding copyright ownership. The ASF licenses this file
6
+ * to you under the Apache License, Version 2.0 (the
7
+ * "License"); you may not use this file except in compliance
8
+ * with the License. You may obtain a copy of the License at
9
+ *
10
+ * http://www.apache.org/licenses/LICENSE-2.0
11
+ *
12
+ * Unless required by applicable law or agreed to in writing,
13
+ * software distributed under the License is distributed on an
14
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15
+ * KIND, either express or implied. See the License for the
16
+ * specific language governing permissions and limitations
17
+ * under the License.
18
+ */
19
+ /**
20
+ * The call-data boundary for one tool execution: the argument views a call
21
+ * produces.
22
+ *
23
+ * Extracted from `executeTool()` so the ownership of each rule is named
24
+ * instead of living inline in a thousand-line method. This module owns
25
+ * *construction only* — admission, dispatch identity, publication timing and
26
+ * persistence stay in `executeTool()`.
27
+ */
28
+ import { computerUseModelCallArgs } from '@maka/core/computer-use';
29
+ /** The recursively frozen, cycle-rejecting argument snapshot. */
30
+ export function snapshotToolArgs(value) {
31
+ return snapshotJsonValue(value, new WeakSet());
32
+ }
33
+ function snapshotJsonValue(value, seen) {
34
+ if (value === null || typeof value !== 'object')
35
+ return value;
36
+ if (seen.has(value))
37
+ throw new Error('Tool arguments must not contain cycles');
38
+ seen.add(value);
39
+ if (Array.isArray(value)) {
40
+ return Object.freeze(value.map((entry) => snapshotJsonValue(entry, seen)));
41
+ }
42
+ const output = {};
43
+ for (const key of Object.keys(value)) {
44
+ const descriptor = Object.getOwnPropertyDescriptor(value, key);
45
+ if (!descriptor || !('value' in descriptor)) {
46
+ throw new Error(`Tool argument ${key} must be a plain data property`);
47
+ }
48
+ output[key] = snapshotJsonValue(descriptor.value, seen);
49
+ }
50
+ return Object.freeze(output);
51
+ }
52
+ /**
53
+ * Validates call arguments against the tool's declared schema, accepting the
54
+ * schema shapes the runtime already supports (zod `safeParseAsync`/`safeParse`,
55
+ * a `validate` callable, or a standard-schema `~standard.validate`). A tool
56
+ * without a usable schema declares nothing and validates nothing.
57
+ */
58
+ export async function validateDeclaredToolArgs(parameters, args) {
59
+ if (!parameters || (typeof parameters !== 'object' && typeof parameters !== 'function')) {
60
+ return;
61
+ }
62
+ const schema = parameters;
63
+ if (typeof schema.safeParseAsync === 'function') {
64
+ const parsed = await schema.safeParseAsync(args);
65
+ if (parsed.success)
66
+ return;
67
+ throw parsed.error;
68
+ }
69
+ if (typeof schema.safeParse === 'function') {
70
+ const parsed = schema.safeParse(args);
71
+ if (parsed.success)
72
+ return;
73
+ throw parsed.error;
74
+ }
75
+ if (typeof schema.validate === 'function') {
76
+ const parsed = await schema.validate(args);
77
+ if (parsed.success)
78
+ return;
79
+ throw parsed.error;
80
+ }
81
+ if (typeof schema['~standard']?.validate === 'function') {
82
+ const parsed = await schema['~standard'].validate(args);
83
+ if ('value' in parsed)
84
+ return;
85
+ throw new Error('Tool arguments failed declared schema validation', { cause: parsed.issues });
86
+ }
87
+ }
88
+ /**
89
+ * The named argument-view construction operation: validation, permission
90
+ * projection, and the persisted view, in the order and with the guards the
91
+ * inline region in `executeTool()` established.
92
+ *
93
+ * The args written into the `tool_start` event and the durable ledger are the
94
+ * record of the call the model reads back on its next turn
95
+ * (`model-history.ts` replays `event.content.args`).
96
+ *
97
+ * Computer Use used the host's approval summary there. That projection exists
98
+ * to decide and display a permission: it renames `window_id` to `windowId`,
99
+ * adds `approvalClass` and `rememberForTurnAllowed`, and drops every argument
100
+ * it does not need. On the real ToolRuntime a model that sent
101
+ * {action:'press_key', app, window_id, observation_id, element_id,
102
+ * text:'cmd+s'} read back {action, approvalClass, rememberForTurnAllowed,
103
+ * app, windowId, observationId} — a key the tool rejects, two fields it never
104
+ * sent, no element, and a press_key with no key. It then went on calling it
105
+ * that way.
106
+ *
107
+ * The permission prompt still reads the permission view, and the approval
108
+ * scope key is still computed from the raw call, so the projection only
109
+ * changes what is written down. `computerUseModelCallArgs` keeps the same
110
+ * privacy rule — screen-derived and user-typed values are reduced to a shape
111
+ * — and speaks the tool's own argument names.
112
+ */
113
+ export async function buildToolCallArgs(input) {
114
+ let permissionArgs = input.executionArgs;
115
+ let permissionArgsError;
116
+ if (!input.directOnlyRejected) {
117
+ try {
118
+ if (!input.validationDeferred) {
119
+ await validateDeclaredToolArgs(input.parameters, input.executionArgs);
120
+ }
121
+ permissionArgs = input.permissionArgs
122
+ ? snapshotToolArgs(input.permissionArgs(structuredClone(input.executionArgs), {
123
+ sessionId: input.sessionId,
124
+ turnId: input.turnId,
125
+ toolCallId: input.toolCallId,
126
+ }))
127
+ : input.executionArgs;
128
+ }
129
+ catch (error) {
130
+ permissionArgsError = error;
131
+ }
132
+ }
133
+ const persistedArgs = input.categoryHint === 'computer_use'
134
+ ? snapshotToolArgs(computerUseModelCallArgs(permissionArgs))
135
+ : permissionArgs;
136
+ return {
137
+ executionArgs: input.executionArgs,
138
+ permissionArgs,
139
+ permissionArgsError,
140
+ persistedArgs,
141
+ };
142
+ }