maka-agent 0.2.0-dev.46.20260922 → 0.2.0-dev.48.20260923

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 (131) hide show
  1. package/DISCLAIMER-WIP +6 -1
  2. package/README.md +6 -1
  3. package/dist/acp/maka-acp-agent.js +1 -1
  4. package/dist/acp/session-mcp.js +212 -0
  5. package/dist/acp/session-registry.js +52 -8
  6. package/dist/acp/stdio-server.js +3 -0
  7. package/dist/cli-core.js +1 -1
  8. package/dist/mcp-capability-provider.js +6 -1
  9. package/dist/mcp-capability-publication.js +139 -0
  10. package/dist/pi-tui-layout.js +46 -17
  11. package/dist/pi-tui-pickers.js +25 -14
  12. package/dist/pi-tui-runner.js +24 -15
  13. package/dist/runtime-host-capability-provider-command.js +25 -6
  14. package/dist/runtime-host-service-manager.js +26 -2
  15. package/dist/tui-context-refresh.js +15 -26
  16. package/dist/tui-mcp-control.js +66 -112
  17. package/native/runtime-host-windows-task-launcher/prebuilds/win32-x64/maka-runtime-host-task-launcher.exe +0 -0
  18. package/node_modules/@maka/acp-executor-plugin/dist/acp-errors.js +26 -0
  19. package/node_modules/@maka/acp-executor-plugin/dist/acp-filesystem.js +103 -0
  20. package/node_modules/@maka/acp-executor-plugin/dist/acp-process.js +119 -0
  21. package/node_modules/@maka/acp-executor-plugin/dist/acp-projection.js +146 -0
  22. package/node_modules/@maka/acp-executor-plugin/dist/index.js +653 -0
  23. package/node_modules/@maka/acp-executor-plugin/dist/plugin.mjs +24446 -0
  24. package/node_modules/@maka/acp-executor-plugin/package.json +15 -0
  25. package/node_modules/@maka/antigravity-acp-plugin/dist/index.js +88 -0
  26. package/node_modules/@maka/antigravity-acp-plugin/dist/model-presentation.js +53 -0
  27. package/node_modules/@maka/antigravity-acp-plugin/dist/plugin.mjs +115 -0
  28. package/node_modules/@maka/antigravity-acp-plugin/package.json +15 -0
  29. package/node_modules/@maka/core/dist/client-capability-grant.js +2 -2
  30. package/node_modules/@maka/core/dist/executor-catalog.js +106 -0
  31. package/node_modules/@maka/core/dist/mcp.js +4 -1
  32. package/node_modules/@maka/core/dist/model-metadata.js +28 -1
  33. package/node_modules/@maka/core/dist/model-thinking.js +36 -1
  34. package/node_modules/@maka/core/dist/provider-registry.js +0 -1
  35. package/node_modules/@maka/core/dist/refresh-read-coordinator.js +62 -0
  36. package/node_modules/@maka/core/dist/runtime-event-store.js +25 -0
  37. package/node_modules/@maka/core/dist/runtime-invocation.js +5 -0
  38. package/node_modules/@maka/core/dist/runtime-policy/connection-catalog-codec.js +4 -0
  39. package/node_modules/@maka/core/dist/runtime-policy/credential-vault-codec.js +6 -0
  40. package/node_modules/@maka/core/dist/runtime-policy/policy-codec.js +21 -0
  41. package/node_modules/@maka/core/dist/session.js +19 -1
  42. package/node_modules/@maka/core/dist/settings.js +3 -0
  43. package/node_modules/@maka/core/dist/tool-ledger-scanner.js +365 -104
  44. package/node_modules/@maka/core/dist/turn-origin.js +14 -0
  45. package/node_modules/@maka/core/package.json +3 -0
  46. package/node_modules/@maka/mcp/dist/credential-coordinator.js +6 -1
  47. package/node_modules/@maka/mcp/dist/index.js +31 -5
  48. package/node_modules/@maka/mcp/dist/oauth.js +54 -6
  49. package/node_modules/@maka/runtime/dist/agent-run-inspect.js +1 -1
  50. package/node_modules/@maka/runtime/dist/ai-sdk-message-projection.js +15 -60
  51. package/node_modules/@maka/runtime/dist/ai-sdk-turn.js +18 -0
  52. package/node_modules/@maka/runtime/dist/apply-patch-profile.js +64 -14
  53. package/node_modules/@maka/runtime/dist/builtin-tools.js +3 -2
  54. package/node_modules/@maka/runtime/dist/codex-v4a-patch.js +18 -0
  55. package/node_modules/@maka/runtime/dist/conversation-copy.js +56 -1
  56. package/node_modules/@maka/runtime/dist/mcp-tools.js +9 -2
  57. package/node_modules/@maka/runtime/dist/model-adapter.js +80 -3
  58. package/node_modules/@maka/runtime/dist/model-runtime.js +6 -0
  59. package/node_modules/@maka/runtime/dist/openai-apply-patch.js +5 -0
  60. package/node_modules/@maka/runtime/dist/plugin-executor-backend.js +122 -3
  61. package/node_modules/@maka/runtime/dist/plugin-executor-service.js +212 -16
  62. package/node_modules/@maka/runtime/dist/runtime-ledger-repair.js +2 -2
  63. package/node_modules/@maka/runtime/dist/session-event-runtime-mapper.js +3 -0
  64. package/node_modules/@maka/runtime/dist/session-manager.js +184 -126
  65. package/node_modules/@maka/runtime/dist/shell-run-manager.js +3 -0
  66. package/node_modules/@maka/runtime/dist/stream-graph-coordinator.js +29 -4
  67. package/node_modules/@maka/runtime-host/dist/candidate-entry.js +8 -5
  68. package/node_modules/@maka/runtime-host/dist/client/capability-provider-service.js +2 -0
  69. package/node_modules/@maka/runtime-host/dist/client/client-capability-channel.js +59 -20
  70. package/node_modules/@maka/runtime-host/dist/client/connection.js +6 -4
  71. package/node_modules/@maka/runtime-host/dist/client/index.js +1 -1
  72. package/node_modules/@maka/runtime-host/dist/client/reconnect-lifecycle.js +8 -1
  73. package/node_modules/@maka/runtime-host/dist/client/reconnecting-connection.js +4 -4
  74. package/node_modules/@maka/runtime-host/dist/client/session-catalog-summary.js +6 -1
  75. package/node_modules/@maka/runtime-host/dist/composition-identity.js +19 -0
  76. package/node_modules/@maka/runtime-host/dist/protocol/client-capability.js +20 -3
  77. package/node_modules/@maka/runtime-host/dist/protocol/external-session.js +8 -5
  78. package/node_modules/@maka/runtime-host/dist/protocol/index.js +20 -2
  79. package/node_modules/@maka/runtime-host/dist/protocol/plugin-platform.js +34 -0
  80. package/node_modules/@maka/runtime-host/dist/protocol/session-catalog.js +75 -5
  81. package/node_modules/@maka/runtime-host/dist/protocol/workhub-coordination.js +1 -5
  82. package/node_modules/@maka/runtime-host/dist/server/builtin-external-agent-plugins.js +188 -0
  83. package/node_modules/@maka/runtime-host/dist/server/candidate.js +12 -7
  84. package/node_modules/@maka/runtime-host/dist/server/client-capability-coordinator.js +234 -70
  85. package/node_modules/@maka/runtime-host/dist/server/client-capability-invocation-broker.js +14 -8
  86. package/node_modules/@maka/runtime-host/dist/server/client-capability-recovery.js +44 -46
  87. package/node_modules/@maka/runtime-host/dist/server/context-coordinator.js +2 -0
  88. package/node_modules/@maka/runtime-host/dist/server/execution-composition-factory.js +1 -1
  89. package/node_modules/@maka/runtime-host/dist/server/execution-composition.js +188 -21
  90. package/node_modules/@maka/runtime-host/dist/server/execution-model-authority.js +6 -0
  91. package/node_modules/@maka/runtime-host/dist/server/extension-bundle.js +4 -0
  92. package/node_modules/@maka/runtime-host/dist/server/external-session-coordinator.js +29 -7
  93. package/node_modules/@maka/runtime-host/dist/server/host-composition-source.js +47 -0
  94. package/node_modules/@maka/runtime-host/dist/server/host-composition.js +1 -29
  95. package/node_modules/@maka/runtime-host/dist/server/host-kernel.js +2 -9
  96. package/node_modules/@maka/runtime-host/dist/server/hosted-execution-recovery.js +75 -9
  97. package/node_modules/@maka/runtime-host/dist/server/hosted-execution-tool-profile.js +5 -1
  98. package/node_modules/@maka/runtime-host/dist/server/interaction-coordinator.js +45 -0
  99. package/node_modules/@maka/runtime-host/dist/server/jev-routing-model.js +157 -0
  100. package/node_modules/@maka/runtime-host/dist/server/listener-set.js +2 -2
  101. package/node_modules/@maka/runtime-host/dist/server/message-coordinator.js +4 -0
  102. package/node_modules/@maka/runtime-host/dist/server/plugin-platform-coordinator.js +11 -1
  103. package/node_modules/@maka/runtime-host/dist/server/process-lifecycle.js +1 -1
  104. package/node_modules/@maka/runtime-host/dist/server/process-termination-error.js +27 -0
  105. package/node_modules/@maka/runtime-host/dist/server/root-turn-coordinator.js +91 -12
  106. package/node_modules/@maka/runtime-host/dist/server/runtime-policy-coordinator.js +2 -0
  107. package/node_modules/@maka/runtime-host/dist/server/session-bundle-coordinator.js +4 -1
  108. package/node_modules/@maka/runtime-host/dist/server/session-catalog-coordinator.js +128 -13
  109. package/node_modules/@maka/runtime-host/dist/server/session-effect-coordinator.js +12 -6
  110. package/node_modules/@maka/runtime-host/dist/server/session-retirement-coordinator.js +2 -2
  111. package/node_modules/@maka/runtime-host/dist/server/session-transcript-reader.js +33 -0
  112. package/node_modules/@maka/runtime-host/dist/server/workhub-coordination-coordinator.js +4 -0
  113. package/node_modules/@maka/runtime-host/dist/server/workhub-result-coordinator.js +173 -0
  114. package/node_modules/@maka/runtime-host/dist/server/workhub-result-runtime.js +293 -0
  115. package/node_modules/@maka/runtime-host/package.json +2 -0
  116. package/node_modules/@maka/storage/dist/agent-run-store-contract.js +8 -0
  117. package/node_modules/@maka/storage/dist/execution-stores.js +15 -3
  118. package/node_modules/@maka/storage/dist/mcp-config-store.js +91 -2
  119. package/node_modules/@maka/storage/dist/operational-state-store.js +15 -1
  120. package/node_modules/@maka/storage/dist/plan-authority.js +2 -0
  121. package/node_modules/@maka/storage/dist/plan-store.js +30 -0
  122. package/node_modules/@maka/storage/dist/runtime-event-persistence.js +1 -0
  123. package/node_modules/@maka/storage/dist/runtime-policy/credential-vault-document.js +4 -1
  124. package/node_modules/@maka/storage/dist/runtime-policy/policy-document.js +2 -0
  125. package/node_modules/@maka/storage/dist/session-store-values.js +16 -4
  126. package/node_modules/@maka/storage/dist/shell-run-authority.js +1 -0
  127. package/node_modules/@maka/storage/dist/shell-run-store.js +15 -0
  128. package/node_modules/@maka/storage/dist/sqlite-runtime-schema.js +74 -1
  129. package/node_modules/@maka/storage/dist/sqlite-runtime-store.js +724 -84
  130. package/node_modules/@maka/storage/dist/sqlite-session-metadata-store.js +16 -0
  131. package/package.json +1 -1
@@ -17,11 +17,12 @@
17
17
  * under the License.
18
18
  */
19
19
  import { createHash, randomUUID } from 'node:crypto';
20
- import { MCP_CONFIG_VERSION, mcpConfigChangeRetiresCredentials, resolveMcpProtocolPreference, } from '@maka/core/mcp';
20
+ import { MCP_CONFIG_VERSION, resolveMcpProtocolPreference, } from '@maka/core/mcp';
21
21
  import { createCredentialMcpOAuthStorage, McpClientManager } from '@maka/mcp';
22
22
  import { createFileCredentialStore } from '@maka/storage/credential-store';
23
- import { AtomicFileWriteCommitUnknownError, createMcpConfigStore, assertMcpEndpointPolicyOnChanges, McpConfigSourceError, normalizeMcpConfig, normalizeMcpImport, } from '@maka/storage/mcp-config-store';
23
+ import { AtomicFileWriteCommitUnknownError, createMcpConfigStore, updateMcpConfiguration, McpConfigurationValidationError, McpConfigSourceError, normalizeMcpConfig, normalizeMcpImport, } from '@maka/storage/mcp-config-store';
24
24
  import { createMcpCapabilityProvider } from './mcp-capability-provider.js';
25
+ import { McpCapabilityPublication } from './mcp-capability-publication.js';
25
26
  const RUNTIME_HOST_CREDENTIAL_ENV = 'MAKA_RUNTIME_HOST_ACCESS_CREDENTIAL';
26
27
  export function createTuiMcpController(input, overrides = {}) {
27
28
  const manager = overrides.manager ??
@@ -42,6 +43,7 @@ class TuiMcpControllerImpl {
42
43
  #listeners = new Set();
43
44
  #disposeManagerChange;
44
45
  #disposeConnectionAvailability;
46
+ #disposeConfigChanges;
45
47
  #initialization;
46
48
  #availability = { kind: 'unavailable' };
47
49
  #closed = false;
@@ -49,9 +51,7 @@ class TuiMcpControllerImpl {
49
51
  #preparedImport;
50
52
  #actionLane = Promise.resolve();
51
53
  #publicationSuppressed = false;
52
- #publicationRequested = false;
53
- #publicationTask;
54
- #published;
54
+ #publication;
55
55
  #snapshot = freezeSnapshot({
56
56
  initialization: 'loading',
57
57
  configuration: 'synchronizing',
@@ -67,6 +67,24 @@ class TuiMcpControllerImpl {
67
67
  ...this.#snapshot,
68
68
  canManagePublicationCredential: Boolean(connection.setCredential && connection.removeCredential),
69
69
  });
70
+ this.#publication = new McpCapabilityPublication({
71
+ connectionIdentity: () => this.#availability.kind === 'connected'
72
+ ? connectionIdentity(this.#availability)
73
+ : undefined,
74
+ revision: () => this.#deps.manager.toolSnapshot().revision,
75
+ createProvider: () => this.#deps.createProvider(this.#deps.manager),
76
+ replace: (provider) => this.#connection.replaceClientCapabilities(provider),
77
+ unregister: () => this.#connection.unregisterClientCapabilities(),
78
+ onState: (state) => {
79
+ this.#updateSnapshot({
80
+ publication: state === 'unavailable'
81
+ ? this.#availability.kind === 'unavailable'
82
+ ? (this.#availability.reason ?? 'host_unavailable')
83
+ : 'waiting'
84
+ : state,
85
+ });
86
+ },
87
+ });
70
88
  this.#disposeManagerChange = deps.manager.onChange(() => {
71
89
  try {
72
90
  this.#refreshManagerSnapshot();
@@ -81,7 +99,7 @@ class TuiMcpControllerImpl {
81
99
  this.#disposeConnectionAvailability = connection.subscribeConnectionAvailability((availability) => {
82
100
  this.#availability = availability;
83
101
  if (availability.kind === 'unavailable') {
84
- this.#published = undefined;
102
+ this.#publication.invalidate();
85
103
  this.#updateSnapshot({
86
104
  publication: availability.reason ?? 'host_unavailable',
87
105
  ...(availability.reason === 'provider_conflict'
@@ -95,6 +113,17 @@ class TuiMcpControllerImpl {
95
113
  this.#requestPublication();
96
114
  }
97
115
  });
116
+ // Desktop edits the same file. A change during startup waits for it
117
+ // rather than being dropped, since startup may have read the file first.
118
+ // Failing to follow leaves the TUI's own edits working, so it stays quiet
119
+ // rather than print over the screen.
120
+ this.#disposeConfigChanges = deps.configStore.subscribeChanges((error) => {
121
+ if (error)
122
+ return;
123
+ void this.#initialization
124
+ .then(() => this.#serializeAction(() => this.#followConfigChange()))
125
+ .catch(() => undefined);
126
+ });
98
127
  this.#initialization = this.#initialize();
99
128
  }
100
129
  snapshot() {
@@ -159,17 +188,14 @@ class TuiMcpControllerImpl {
159
188
  this.#closed = true;
160
189
  this.#disposeManagerChange();
161
190
  this.#disposeConnectionAvailability();
191
+ this.#disposeConfigChanges();
162
192
  this.#listeners.clear();
163
193
  this.#preparedImport = undefined;
164
- this.#publicationRequested = false;
194
+ const publicationClosing = this.#publication.close().catch(() => undefined);
165
195
  const managerClosing = this.#deps.manager.close();
166
196
  await this.#actionLane.catch(() => undefined);
167
197
  this.#config = undefined;
168
- await this.#publicationTask?.catch(() => undefined);
169
- if (this.#availability.kind === 'connected') {
170
- await this.#connection.unregisterClientCapabilities().catch(() => undefined);
171
- }
172
- this.#published = undefined;
198
+ await publicationClosing;
173
199
  await this.#connection.closePublication?.().catch(() => undefined);
174
200
  await managerClosing;
175
201
  await this.#initialization.catch(() => undefined);
@@ -251,44 +277,32 @@ class TuiMcpControllerImpl {
251
277
  async #commitMutation(action) {
252
278
  let committed;
253
279
  try {
254
- committed = await this.#deps.configStore.transform(async (current) => {
255
- if (this.#closed) {
280
+ committed = await updateMcpConfiguration(this.#deps.configStore, (current) => {
281
+ if (this.#closed)
256
282
  throw new TuiMcpMutationError({ status: 'failed', reason: 'closed' });
257
- }
258
283
  const prepared = this.#prepareMutation(current, action);
259
284
  if ('status' in prepared)
260
285
  throw new TuiMcpMutationError(prepared);
261
- const { next } = prepared;
286
+ return prepared.next;
287
+ }, async (serverId, previous) => {
262
288
  try {
263
- assertMcpEndpointPolicyOnChanges(current, next);
289
+ await this.#deps.manager.forgetServerCredentials(serverId, previous);
264
290
  }
265
291
  catch {
266
- throw new TuiMcpMutationError({ status: 'failed', reason: 'invalid-config' });
267
- }
268
- try {
269
- for (const [serverId, previous] of Object.entries(current.mcpServers)) {
270
- if (!mcpConfigChangeRetiresCredentials(previous, next.mcpServers[serverId]))
271
- continue;
272
- await this.#deps.manager.forgetServerCredentials(serverId, previous);
273
- if (this.#closed) {
274
- throw new TuiMcpMutationError({ status: 'failed', reason: 'closed' });
275
- }
276
- }
277
- }
278
- catch (error) {
279
- if (error instanceof TuiMcpMutationError)
280
- throw error;
281
292
  throw new TuiMcpMutationError({
282
293
  status: 'failed',
283
294
  reason: 'credential-cleanup-failed',
284
295
  });
285
296
  }
286
- return next;
297
+ if (this.#closed)
298
+ throw new TuiMcpMutationError({ status: 'failed', reason: 'closed' });
287
299
  });
288
300
  }
289
301
  catch (error) {
290
302
  if (error instanceof TuiMcpMutationError)
291
303
  return error.result;
304
+ if (error instanceof McpConfigurationValidationError)
305
+ return { status: 'failed', reason: 'invalid-config' };
292
306
  if (error instanceof AtomicFileWriteCommitUnknownError) {
293
307
  // The transform has already published, including any credential
294
308
  // retirement. Reload its authority; never replay those effects.
@@ -323,12 +337,24 @@ class TuiMcpControllerImpl {
323
337
  }
324
338
  return { status: 'failed', reason: 'persist-failed' };
325
339
  }
340
+ this.#preparedImport = undefined;
326
341
  return (await this.#synchronizeCommittedConfig(committed)).result;
327
342
  }
343
+ async #followConfigChange() {
344
+ if (this.#closed)
345
+ return;
346
+ if (this.#snapshot.initialization === 'error')
347
+ return this.#initialize();
348
+ const latest = await this.#deps.configStore.get();
349
+ // An import preview survives: its commit re-checks each server it replaces.
350
+ if (this.#snapshot.configuration !== 'ready' ||
351
+ JSON.stringify(latest) !== JSON.stringify(this.#config)) {
352
+ await this.#synchronizeCommittedConfig(latest);
353
+ }
354
+ }
328
355
  async #synchronizeCommittedConfig(committed) {
329
356
  if (this.#closed)
330
357
  return { result: { status: 'failed', reason: 'closed' } };
331
- this.#preparedImport = undefined;
332
358
  this.#config = cloneConfig(committed);
333
359
  this.#updateSnapshot({ configuration: 'synchronizing' });
334
360
  this.#refreshManagerSnapshot();
@@ -400,9 +426,10 @@ class TuiMcpControllerImpl {
400
426
  }
401
427
  }
402
428
  async #settlePublication() {
403
- this.#requestPublication();
404
- while (!this.#closed && (this.#publicationTask || this.#publicationRequested)) {
405
- await this.#publicationTask?.catch(() => undefined);
429
+ if (!this.#closed &&
430
+ this.#snapshot.initialization === 'ready' &&
431
+ !this.#publicationSuppressed) {
432
+ await this.#publication.settle();
406
433
  }
407
434
  if (this.#snapshot.publication === 'error' ||
408
435
  this.#snapshot.publication === 'credential_rejected' ||
@@ -436,74 +463,9 @@ class TuiMcpControllerImpl {
436
463
  this.#notify();
437
464
  }
438
465
  #requestPublication() {
439
- if (this.#closed) {
440
- this.#publicationRequested = false;
466
+ if (this.#closed || this.#snapshot.initialization !== 'ready' || this.#publicationSuppressed)
441
467
  return;
442
- }
443
- if (this.#snapshot.initialization !== 'ready' || this.#publicationSuppressed)
444
- return;
445
- this.#publicationRequested = true;
446
- if (this.#publicationTask)
447
- return;
448
- this.#publicationTask = this.#runPublicationQueue().finally(() => {
449
- this.#publicationTask = undefined;
450
- if (this.#publicationRequested)
451
- this.#requestPublication();
452
- });
453
- }
454
- async #runPublicationQueue() {
455
- while (this.#publicationRequested && !this.#closed) {
456
- this.#publicationRequested = false;
457
- await this.#publishCurrentSnapshot();
458
- }
459
- }
460
- async #publishCurrentSnapshot() {
461
- const availability = this.#availability;
462
- if (availability.kind !== 'connected') {
463
- this.#updateSnapshot({ publication: availability.reason ?? 'host_unavailable' });
464
- return;
465
- }
466
- const identity = connectionIdentity(availability);
467
- const revision = this.#deps.manager.toolSnapshot().revision;
468
- if (this.#published?.identity === identity && this.#published.revision === revision) {
469
- this.#updateSnapshot({
470
- publication: this.#snapshot.toolCount === 0 ? 'not_published' : 'published',
471
- });
472
- return;
473
- }
474
- let provider;
475
- this.#updateSnapshot({ publication: 'publishing' });
476
- try {
477
- provider = this.#deps.createProvider(this.#deps.manager);
478
- if (provider) {
479
- await this.#connection.replaceClientCapabilities(provider);
480
- }
481
- else if (this.#published?.identity === identity && this.#published.registered) {
482
- await this.#connection.unregisterClientCapabilities();
483
- }
484
- }
485
- catch {
486
- await closeProvider(provider);
487
- if (this.#isCurrent(identity, revision)) {
488
- this.#updateSnapshot({ publication: 'error' });
489
- }
490
- else {
491
- this.#requestPublication();
492
- }
493
- return;
494
- }
495
- if (!this.#isCurrent(identity, revision)) {
496
- this.#requestPublication();
497
- return;
498
- }
499
- this.#published = { identity, revision, registered: provider !== undefined };
500
- this.#updateSnapshot({ publication: provider ? 'published' : 'not_published' });
501
- }
502
- #isCurrent(identity, revision) {
503
- return (!this.#closed &&
504
- this.#availability.kind === 'connected' &&
505
- connectionIdentity(this.#availability) === identity &&
506
- this.#deps.manager.toolSnapshot().revision === revision);
468
+ this.#publication.request();
507
469
  }
508
470
  #updateSnapshot(update) {
509
471
  this.#snapshot = freezeSnapshot({ ...this.#snapshot, ...update });
@@ -548,14 +510,6 @@ function freezeSnapshot(snapshot) {
548
510
  function connectionIdentity(availability) {
549
511
  return `${availability.hostEpoch}\0${availability.connectionId}`;
550
512
  }
551
- async function closeProvider(provider) {
552
- try {
553
- await provider?.close?.();
554
- }
555
- catch {
556
- // A rejected provider never crossed into Host ownership.
557
- }
558
- }
559
513
  function cloneConfig(config) {
560
514
  return structuredClone(config);
561
515
  }
@@ -0,0 +1,26 @@
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
+ export class AcpRuntimeError extends Error {
20
+ code;
21
+ constructor(message, code) {
22
+ super(message);
23
+ this.code = code;
24
+ this.name = 'AcpRuntimeError';
25
+ }
26
+ }
@@ -0,0 +1,103 @@
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
+ import { constants } from 'node:fs';
20
+ import { lstat, open, realpath, stat } from 'node:fs/promises';
21
+ import { dirname, isAbsolute, relative, resolve } from 'node:path';
22
+ const MAX_TEXT_FILE_BYTES = 8 * 1024 * 1024;
23
+ export async function readWorkspaceTextFile(cwd, path, line, limit) {
24
+ if (line != null && (!Number.isSafeInteger(line) || line < 1))
25
+ throw new Error('ACP file line must be a positive integer');
26
+ if (limit != null && (!Number.isSafeInteger(limit) || limit < 0))
27
+ throw new Error('ACP file limit must be a non-negative integer');
28
+ const file = await openWorkspaceFile(cwd, path, false);
29
+ try {
30
+ const info = await file.stat();
31
+ if (info.size > MAX_TEXT_FILE_BYTES)
32
+ throw new Error('ACP text file is too large');
33
+ const text = await file.readFile('utf8');
34
+ const start = line == null ? 0 : line - 1;
35
+ return line != null || limit != null
36
+ ? text
37
+ .split('\n')
38
+ .slice(start, limit == null ? undefined : start + limit)
39
+ .join('\n')
40
+ : text;
41
+ }
42
+ finally {
43
+ await file.close();
44
+ }
45
+ }
46
+ export async function writeWorkspaceTextFile(cwd, path, content) {
47
+ if (Buffer.byteLength(content) > MAX_TEXT_FILE_BYTES)
48
+ throw new Error('ACP text file is too large');
49
+ const file = await openWorkspaceFile(cwd, path, true);
50
+ try {
51
+ // Validation happens against this exact open file descriptor before any
52
+ // existing content is truncated, so later path swaps cannot redirect data.
53
+ await file.truncate(0);
54
+ await file.writeFile(content, 'utf8');
55
+ }
56
+ finally {
57
+ await file.close();
58
+ }
59
+ }
60
+ async function openWorkspaceFile(cwd, path, forWrite) {
61
+ if (!isAbsolute(path))
62
+ throw new Error('ACP file path must be absolute');
63
+ const root = await realpath(cwd);
64
+ const candidate = resolve(path);
65
+ if (forWrite) {
66
+ const parent = await realpath(dirname(candidate));
67
+ assertContained(root, parent);
68
+ const entry = await lstat(candidate).catch((error) => {
69
+ if (error.code !== 'ENOENT')
70
+ throw error;
71
+ return undefined;
72
+ });
73
+ if (entry?.isSymbolicLink())
74
+ throw new Error('ACP file path could not be resolved');
75
+ }
76
+ const flags = forWrite
77
+ ? constants.O_WRONLY | constants.O_CREAT | (constants.O_NOFOLLOW ?? 0)
78
+ : constants.O_RDONLY | (constants.O_NOFOLLOW ?? 0);
79
+ const file = await open(candidate, flags, 0o600);
80
+ try {
81
+ const opened = await file.stat();
82
+ if (!opened.isFile())
83
+ throw new Error('ACP file path is not a regular file');
84
+ // Resolve and compare after opening. If the path or an ancestor changed
85
+ // between lookup and open, either containment or file identity no longer
86
+ // matches. Operations after this point use only the verified descriptor.
87
+ const canonical = await realpath(candidate);
88
+ assertContained(root, canonical);
89
+ const current = await stat(canonical);
90
+ if (opened.dev !== current.dev || opened.ino !== current.ino)
91
+ throw new Error('ACP file path changed during validation');
92
+ return file;
93
+ }
94
+ catch (error) {
95
+ await file.close();
96
+ throw error;
97
+ }
98
+ }
99
+ function assertContained(root, candidate) {
100
+ const relation = relative(root, candidate);
101
+ if (relation === '..' || relation.startsWith('../') || isAbsolute(relation))
102
+ throw new Error('ACP file path leaves the workspace');
103
+ }
@@ -0,0 +1,119 @@
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
+ import { spawn } from 'node:child_process';
20
+ import { Readable, Writable } from 'node:stream';
21
+ import { setTimeout as delay } from 'node:timers/promises';
22
+ import { client, ndJsonStream, } from '@agentclientprotocol/sdk';
23
+ import { terminateProcessTree } from '@maka/runtime/process-tree-terminator';
24
+ import { AcpRuntimeError } from './acp-errors.js';
25
+ const PROCESS_EXIT_TIMEOUT_MS = 2_000;
26
+ export function createAcpConnection(input) {
27
+ let child;
28
+ try {
29
+ child = spawn(input.executable, [...(input.args ?? [])], {
30
+ cwd: input.cwd,
31
+ env: input.env,
32
+ stdio: 'pipe',
33
+ detached: true,
34
+ shell: false,
35
+ });
36
+ }
37
+ catch {
38
+ throw new AcpRuntimeError('ACP executable is unavailable', 'acp_executable_unavailable');
39
+ }
40
+ let disposing = false;
41
+ let disposed = false;
42
+ let disposal;
43
+ let rejectFailure;
44
+ const failed = new Promise((_resolve, reject) => {
45
+ rejectFailure = reject;
46
+ });
47
+ void failed.catch(() => undefined);
48
+ const fail = () => {
49
+ if (!disposing)
50
+ rejectFailure(new Error('ACP connection failed'));
51
+ };
52
+ child.once('error', fail);
53
+ child.stdin.once('error', fail);
54
+ child.stderr.once('error', fail);
55
+ child.stderr.on('data', () => undefined);
56
+ child.once('close', fail);
57
+ const app = client({ name: input.clientName });
58
+ input.configureClient(app);
59
+ const connection = app.connect(ndJsonStream(Writable.toWeb(child.stdin), Readable.toWeb(child.stdout)));
60
+ void connection.closed.then(fail, fail);
61
+ return {
62
+ connection,
63
+ failed,
64
+ dispose() {
65
+ if (disposed)
66
+ return Promise.resolve();
67
+ if (disposal)
68
+ return disposal;
69
+ disposing = true;
70
+ disposal = terminate(child, connection).then(() => {
71
+ disposed = true;
72
+ }, (error) => {
73
+ disposal = undefined;
74
+ throw error;
75
+ });
76
+ return disposal;
77
+ },
78
+ };
79
+ }
80
+ async function terminate(child, connection) {
81
+ const pid = child.pid;
82
+ const alive = () => {
83
+ if (!pid)
84
+ return false;
85
+ if (process.platform === 'win32')
86
+ return child.exitCode === null && child.signalCode === null;
87
+ try {
88
+ process.kill(-pid, 0);
89
+ return true;
90
+ }
91
+ catch (error) {
92
+ return error.code !== 'ESRCH';
93
+ }
94
+ };
95
+ try {
96
+ if (pid) {
97
+ await terminateProcessTree({ pid, signal: 'SIGTERM', fallback: () => child.kill('SIGTERM') });
98
+ for (let elapsed = 0; elapsed < PROCESS_EXIT_TIMEOUT_MS && alive(); elapsed += 50)
99
+ await delay(50);
100
+ if (alive()) {
101
+ await terminateProcessTree({
102
+ pid,
103
+ signal: 'SIGKILL',
104
+ fallback: () => child.kill('SIGKILL'),
105
+ });
106
+ for (let elapsed = 0; elapsed < PROCESS_EXIT_TIMEOUT_MS && alive(); elapsed += 50)
107
+ await delay(50);
108
+ }
109
+ if (alive())
110
+ throw new Error('ACP process cleanup failed');
111
+ }
112
+ }
113
+ finally {
114
+ connection.close();
115
+ child.stdin.destroy();
116
+ child.stdout.destroy();
117
+ child.stderr.destroy();
118
+ }
119
+ }
@@ -0,0 +1,146 @@
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
+ const MAX_EVENT_TEXT = 8_192;
20
+ const MAX_TOOL_RESULT_DIFF = 1024 * 1024;
21
+ export function promptText(request) {
22
+ const sections = [request.text];
23
+ if (request.instructions)
24
+ sections.push(`Agent instructions:\n${request.instructions}`);
25
+ for (const quote of request.quotes ?? [])
26
+ sections.push(`Quoted context:\n${quote.text}`);
27
+ for (const reference of request.directoryReferences ?? [])
28
+ sections.push(`Project directory reference: ${reference.path}`);
29
+ return sections.filter(Boolean).join('\n\n');
30
+ }
31
+ export function emitText(context, type, text) {
32
+ const safeText = text.replaceAll('\r', '');
33
+ for (let offset = 0; offset < safeText.length; offset += MAX_EVENT_TEXT) {
34
+ const chunk = safeText.slice(offset, offset + MAX_EVENT_TEXT);
35
+ context.emit({ type, text: chunk });
36
+ }
37
+ }
38
+ export function emitToolOutput(context, toolCallId, text) {
39
+ const safeText = text.replaceAll('\r', '');
40
+ for (let offset = 0; offset < safeText.length; offset += MAX_EVENT_TEXT) {
41
+ context.emit({
42
+ type: 'tool_output_delta',
43
+ toolCallId,
44
+ text: safeText.slice(offset, offset + MAX_EVENT_TEXT),
45
+ });
46
+ }
47
+ }
48
+ export function projectToolResult(content, rawOutput) {
49
+ const diffs = content.flatMap((item) => item.type === 'diff'
50
+ ? [
51
+ {
52
+ path: item.path,
53
+ diff: createWholeFileDiff(item.path, item.oldText ?? '', item.newText ?? ''),
54
+ },
55
+ ]
56
+ : []);
57
+ const combinedDiff = diffs.map(({ diff }) => diff).join('');
58
+ if (diffs.length && diffs.length <= 64 && combinedDiff.length <= MAX_TOOL_RESULT_DIFF)
59
+ return {
60
+ kind: 'file_diff',
61
+ paths: diffs.map(({ path }) => path),
62
+ diff: combinedDiff,
63
+ };
64
+ if (diffs.length)
65
+ return {
66
+ kind: 'text',
67
+ text: boundedText(`${diffs.map(({ path }) => `Updated ${path}`).join('\n')}\nDiff omitted because it exceeds the executor event limit.`),
68
+ };
69
+ return { kind: 'text', text: boundedText(summarizeToolResult(content, rawOutput)) };
70
+ }
71
+ function createWholeFileDiff(path, oldText, newText) {
72
+ const oldFile = diffLines(oldText);
73
+ const newFile = diffLines(newText);
74
+ const oldLines = oldFile.lines;
75
+ const newLines = newFile.lines;
76
+ return ([
77
+ `--- a/${path}`,
78
+ `+++ b/${path}`,
79
+ `@@ -${oldLines.length ? 1 : 0},${oldLines.length} +${newLines.length ? 1 : 0},${newLines.length} @@`,
80
+ ...diffSide(oldLines, '-', oldFile.endsWithNewline),
81
+ ...diffSide(newLines, '+', newFile.endsWithNewline),
82
+ ].join('\n') + '\n');
83
+ }
84
+ function diffLines(text) {
85
+ const normalized = text.replaceAll('\r', '');
86
+ if (!normalized)
87
+ return { lines: [], endsWithNewline: true };
88
+ const endsWithNewline = normalized.endsWith('\n');
89
+ const lines = normalized.split('\n');
90
+ if (endsWithNewline)
91
+ lines.pop();
92
+ return { lines, endsWithNewline };
93
+ }
94
+ function diffSide(lines, prefix, endsWithNewline) {
95
+ return lines.flatMap((line, index) => [
96
+ `${prefix}${line}`,
97
+ ...(index === lines.length - 1 && !endsWithNewline ? ['\'] : []),
98
+ ]);
99
+ }
100
+ export function summarizeToolContent(content) {
101
+ return content
102
+ .map((item) => {
103
+ if (item.type === 'diff')
104
+ return `Updated ${item.path}`;
105
+ if (item.type === 'terminal')
106
+ return item.terminalId ? `Terminal ${item.terminalId}` : '';
107
+ return item.content.type === 'text' ? item.content.text : '';
108
+ })
109
+ .filter(Boolean)
110
+ .join('\n');
111
+ }
112
+ function summarizeToolResult(content, rawOutput) {
113
+ const summary = summarizeToolContent(content);
114
+ if (summary)
115
+ return summary;
116
+ if (rawOutput === undefined)
117
+ return '';
118
+ try {
119
+ return JSON.stringify(rawOutput);
120
+ }
121
+ catch {
122
+ return 'External tool completed';
123
+ }
124
+ }
125
+ export function boundedText(value) {
126
+ const safe = value.replace(/[\0\r]/gu, '');
127
+ return safe.length <= MAX_EVENT_TEXT ? safe : `${safe.slice(0, MAX_EVENT_TEXT - 1)}…`;
128
+ }
129
+ export function toolName(value) {
130
+ return value.replace(/[\0\r\n]/gu, '').slice(0, 256) || 'external_tool';
131
+ }
132
+ export function activityKind(kind) {
133
+ if (kind === 'read')
134
+ return 'read';
135
+ if (kind === 'edit' || kind === 'delete' || kind === 'move')
136
+ return 'edit';
137
+ if (kind === 'search')
138
+ return 'search';
139
+ if (kind === 'fetch')
140
+ return 'webfetch';
141
+ if (kind === 'execute')
142
+ return 'command';
143
+ if (kind === 'think')
144
+ return 'explore';
145
+ return 'tool';
146
+ }