@yuandc/aica 0.1.0 → 0.1.2

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 (48) hide show
  1. package/dist/acp/agent.js +1 -54
  2. package/dist/acp/client/acp-client.js +1 -102
  3. package/dist/acp/client/acp-content.js +1 -13
  4. package/dist/acp/client/acp-events.js +1 -106
  5. package/dist/acp/client/acp-process.js +1 -34
  6. package/dist/acp/client/acp-runtime-pool.js +1 -248
  7. package/dist/acp/client/context-usage.js +1 -29
  8. package/dist/acp/client/json-rpc.js +4 -128
  9. package/dist/acp/provider-types.js +0 -1
  10. package/dist/acp/providers/codex/codex-process.js +1 -51
  11. package/dist/acp/providers/codex/events.js +28 -1473
  12. package/dist/acp/providers/codex/permissions.js +1 -49
  13. package/dist/acp/providers/codex/provider.js +1 -376
  14. package/dist/acp/providers/codex-acp/adapter.js +5 -947
  15. package/dist/acp/providers/codex-acp/context-maintenance.js +5 -148
  16. package/dist/acp/providers/codex-acp/launch.js +1 -35
  17. package/dist/acp/providers/codex-acp/provider.js +1 -486
  18. package/dist/acp/providers/mimo/provider.js +5 -448
  19. package/dist/acp/providers/opencode/provider.js +4 -489
  20. package/dist/acp/providers/registry.js +1 -23
  21. package/dist/acp/standard-events.js +1 -167
  22. package/dist/commands/start.js +1 -137
  23. package/dist/commands/worker-auth.js +4 -100
  24. package/dist/commands/worker-project.js +1 -57
  25. package/dist/core/aca-config.js +1 -74
  26. package/dist/core/aca-server-client.js +1 -57
  27. package/dist/core/acp-event-coalescer.js +1 -108
  28. package/dist/core/acp-event-upload-filter.js +1 -16
  29. package/dist/core/acp-orphan-cleanup.js +1 -91
  30. package/dist/core/affected-files.js +2 -268
  31. package/dist/core/auth.js +1 -36
  32. package/dist/core/file-transfer-worker.js +1 -169
  33. package/dist/core/fs.js +2 -28
  34. package/dist/core/heartbeat.js +3 -578
  35. package/dist/core/job-permission-policy.js +1 -42
  36. package/dist/core/job-worker.js +6 -749
  37. package/dist/core/logger.js +3 -42
  38. package/dist/core/long-poll-worker.js +1 -26
  39. package/dist/core/machine-filesystem-worker.js +3 -352
  40. package/dist/core/paths.js +1 -26
  41. package/dist/core/process-identity.js +1 -34
  42. package/dist/core/process.js +2 -33
  43. package/dist/core/provider-health.js +1 -54
  44. package/dist/core/runtime-options.js +1 -38
  45. package/dist/core/worktree.js +1 -95
  46. package/dist/worker-cli.js +1 -26
  47. package/dist/worker-single-cli.js +1 -16
  48. package/package.json +1 -1
package/dist/acp/agent.js CHANGED
@@ -1,54 +1 @@
1
- import fs from "node:fs";
2
- import { createAgentProvider } from "./providers/registry.js";
3
- export async function sendAgentPrompt(input) {
4
- if (!fs.existsSync(input.cwd) || !fs.statSync(input.cwd).isDirectory()) {
5
- throw new Error(`Project root does not exist or is not a directory: ${input.cwd}`);
6
- }
7
- const provider = createAgentProvider({
8
- agentType: input.agentType || "codex",
9
- cliType: input.cliType || "builtin"
10
- });
11
- const session = await provider.createSession({
12
- cwd: input.cwd,
13
- providerSessionId: input.acpSessionId ?? null,
14
- timeoutMs: input.timeoutMs
15
- });
16
- try {
17
- const result = await session.sendPrompt({
18
- ...input,
19
- providerSessionId: input.acpSessionId ?? null
20
- });
21
- return {
22
- ...result,
23
- acpSessionId: result.providerSessionId
24
- };
25
- }
26
- finally {
27
- await session.close().catch(() => void 0);
28
- }
29
- }
30
- export async function compactAgentContext(input) {
31
- const provider = createAgentProvider({
32
- agentType: input.agentType || "codex",
33
- cliType: input.cliType || "builtin"
34
- });
35
- const session = await provider.createSession({
36
- cwd: input.cwd,
37
- providerSessionId: input.acpSessionId,
38
- timeoutMs: input.timeoutMs
39
- });
40
- if (!session.compactContext) {
41
- await session.close().catch(() => void 0);
42
- throw new Error(`Provider does not support context compaction: ${input.agentType || "codex"}`);
43
- }
44
- try {
45
- const result = await session.compactContext(input);
46
- return {
47
- ...result,
48
- acpSessionId: result.providerSessionId
49
- };
50
- }
51
- finally {
52
- await session.close().catch(() => void 0);
53
- }
54
- }
1
+ import s from"node:fs";import{createAgentProvider as r}from"./providers/registry.js";async function a(e){if(!s.existsSync(e.cwd)||!s.statSync(e.cwd).isDirectory())throw new Error(`Project root does not exist or is not a directory: ${e.cwd}`);const o=await r({agentType:e.agentType||"codex",cliType:e.cliType||"builtin"}).createSession({cwd:e.cwd,providerSessionId:e.acpSessionId??null,timeoutMs:e.timeoutMs});try{const t=await o.sendPrompt({...e,providerSessionId:e.acpSessionId??null});return{...t,acpSessionId:t.providerSessionId}}finally{await o.close().catch(()=>{})}}async function d(e){const o=await r({agentType:e.agentType||"codex",cliType:e.cliType||"builtin"}).createSession({cwd:e.cwd,providerSessionId:e.acpSessionId,timeoutMs:e.timeoutMs});if(!o.compactContext)throw await o.close().catch(()=>{}),new Error(`Provider does not support context compaction: ${e.agentType||"codex"}`);try{const t=await o.compactContext(e);return{...t,acpSessionId:t.providerSessionId}}finally{await o.close().catch(()=>{})}}export{d as compactAgentContext,a as sendAgentPrompt};
@@ -1,102 +1 @@
1
- import { JsonLineRpcClient } from "./json-rpc.js";
2
- export class AcpClient {
3
- onSessionUpdate;
4
- onClientRequest;
5
- rpc;
6
- constructor(child, onSessionUpdate, onClientRequest) {
7
- this.onSessionUpdate = onSessionUpdate;
8
- this.onClientRequest = onClientRequest;
9
- this.rpc = new JsonLineRpcClient(child, {
10
- peerName: "ACP server",
11
- includeJsonRpc: true,
12
- onNotification: (method, params, message) => {
13
- if (method === "session/update")
14
- this.onSessionUpdate(params, message);
15
- },
16
- onRequest: async (method, params, message) => {
17
- if (this.onClientRequest)
18
- return this.onClientRequest(method, params, message);
19
- throw new Error(`Unsupported ACP client request: ${method}`);
20
- }
21
- });
22
- }
23
- initialize(timeoutMs = 60_000) {
24
- return this.rpc.request("initialize", {
25
- protocolVersion: 1,
26
- clientCapabilities: {
27
- fs: { readTextFile: false, writeTextFile: false },
28
- terminal: false
29
- },
30
- clientInfo: { name: "aca", version: "0.1.0" }
31
- }, timeoutMs);
32
- }
33
- newSession(input, timeoutMs = 120_000) {
34
- return this.rpc.request("session/new", {
35
- cwd: input.cwd,
36
- mcpServers: input.mcpServers ?? [],
37
- ...(input.config ? { config: input.config } : {})
38
- }, timeoutMs);
39
- }
40
- resumeSession(input, timeoutMs = 120_000) {
41
- return this.rpc.request("session/resume", {
42
- sessionId: input.sessionId,
43
- cwd: input.cwd,
44
- mcpServers: input.mcpServers ?? [],
45
- ...(input.config ? { config: input.config } : {})
46
- }, timeoutMs);
47
- }
48
- loadSession(input, timeoutMs = 120_000) {
49
- return this.rpc.request("session/load", {
50
- sessionId: input.sessionId,
51
- cwd: input.cwd,
52
- mcpServers: input.mcpServers ?? [],
53
- ...(input.config ? { config: input.config } : {})
54
- }, timeoutMs);
55
- }
56
- closeSession(sessionId, timeoutMs = 30_000) {
57
- return this.rpc.request("session/close", { sessionId }, timeoutMs);
58
- }
59
- listSessions(params = {}, timeoutMs = 60_000) {
60
- return this.rpc.request("session/list", params, timeoutMs);
61
- }
62
- deleteSession(sessionId, timeoutMs = 60_000) {
63
- return this.rpc.request("session/delete", { sessionId }, timeoutMs);
64
- }
65
- forkSession(input, timeoutMs = 120_000) {
66
- return this.rpc.request("session/fork", {
67
- sessionId: input.sessionId,
68
- ...(input.cwd ? { cwd: input.cwd } : {}),
69
- ...(input.mcpServers ? { mcpServers: input.mcpServers } : {})
70
- }, timeoutMs);
71
- }
72
- setSessionConfigOption(input, timeoutMs = 60_000) {
73
- return this.rpc.request("session/set_config_option", input, timeoutMs);
74
- }
75
- setSessionMode(input, timeoutMs = 60_000) {
76
- return this.rpc.request("session/set_mode", input, timeoutMs);
77
- }
78
- prompt(input, timeoutMs) {
79
- return this.rpc.request("session/prompt", input, timeoutMs);
80
- }
81
- compactSession(input, timeoutMs = 10 * 60 * 1000) {
82
- return this.rpc.request("session/compact", input, timeoutMs);
83
- }
84
- cancel(sessionId) {
85
- this.rpc.notify("session/cancel", { sessionId });
86
- }
87
- authenticate(input, timeoutMs = 120_000) {
88
- return this.rpc.request("authenticate", input, timeoutMs);
89
- }
90
- logout(input = {}, timeoutMs = 60_000) {
91
- return this.rpc.request("logout", input, timeoutMs);
92
- }
93
- listProviders(input = {}, timeoutMs = 60_000) {
94
- return this.rpc.request("providers/list", input, timeoutMs);
95
- }
96
- setProvider(input, timeoutMs = 60_000) {
97
- return this.rpc.request("providers/set", input, timeoutMs);
98
- }
99
- disableProvider(input, timeoutMs = 60_000) {
100
- return this.rpc.request("providers/disable", input, timeoutMs);
101
- }
102
- }
1
+ import{JsonLineRpcClient as n}from"./json-rpc.js";class l{onSessionUpdate;onClientRequest;rpc;constructor(e,s,o){this.onSessionUpdate=s,this.onClientRequest=o,this.rpc=new n(e,{peerName:"ACP server",includeJsonRpc:!0,onNotification:(r,t,i)=>{r==="session/update"&&this.onSessionUpdate(t,i)},onRequest:async(r,t,i)=>{if(this.onClientRequest)return this.onClientRequest(r,t,i);throw new Error(`Unsupported ACP client request: ${r}`)}})}initialize(e=6e4){return this.rpc.request("initialize",{protocolVersion:1,clientCapabilities:{fs:{readTextFile:!1,writeTextFile:!1},terminal:!1},clientInfo:{name:"aca",version:"0.1.0"}},e)}newSession(e,s=12e4){return this.rpc.request("session/new",{cwd:e.cwd,mcpServers:e.mcpServers??[],...e.config?{config:e.config}:{}},s)}resumeSession(e,s=12e4){return this.rpc.request("session/resume",{sessionId:e.sessionId,cwd:e.cwd,mcpServers:e.mcpServers??[],...e.config?{config:e.config}:{}},s)}loadSession(e,s=12e4){return this.rpc.request("session/load",{sessionId:e.sessionId,cwd:e.cwd,mcpServers:e.mcpServers??[],...e.config?{config:e.config}:{}},s)}closeSession(e,s=3e4){return this.rpc.request("session/close",{sessionId:e},s)}listSessions(e={},s=6e4){return this.rpc.request("session/list",e,s)}deleteSession(e,s=6e4){return this.rpc.request("session/delete",{sessionId:e},s)}forkSession(e,s=12e4){return this.rpc.request("session/fork",{sessionId:e.sessionId,...e.cwd?{cwd:e.cwd}:{},...e.mcpServers?{mcpServers:e.mcpServers}:{}},s)}setSessionConfigOption(e,s=6e4){return this.rpc.request("session/set_config_option",e,s)}setSessionMode(e,s=6e4){return this.rpc.request("session/set_mode",e,s)}prompt(e,s){return this.rpc.request("session/prompt",e,s)}compactSession(e,s=600*1e3){return this.rpc.request("session/compact",e,s)}cancel(e){this.rpc.notify("session/cancel",{sessionId:e})}authenticate(e,s=12e4){return this.rpc.request("authenticate",e,s)}logout(e={},s=6e4){return this.rpc.request("logout",e,s)}listProviders(e={},s=6e4){return this.rpc.request("providers/list",e,s)}setProvider(e,s=6e4){return this.rpc.request("providers/set",e,s)}disableProvider(e,s=6e4){return this.rpc.request("providers/disable",e,s)}}export{l as AcpClient};
@@ -1,13 +1 @@
1
- import fs from "node:fs";
2
- export function promptBlocksToAcpContent(prompt, blocks) {
3
- const inputBlocks = blocks?.length ? blocks : [{ type: "text", text: prompt }];
4
- return inputBlocks.map((block) => {
5
- if (block.type === "image") {
6
- if (block.uri && fs.existsSync(block.uri)) {
7
- return { type: "image", uri: block.uri, mimeType: block.mimeType };
8
- }
9
- return { type: "image", data: block.data, mimeType: block.mimeType };
10
- }
11
- return { type: "text", text: block.text };
12
- });
13
- }
1
+ import p from"node:fs";function n(i,e){return(e?.length?e:[{type:"text",text:i}]).map(t=>t.type==="image"?t.uri&&p.existsSync(t.uri)?{type:"image",uri:t.uri,mimeType:t.mimeType}:{type:"image",data:t.data,mimeType:t.mimeType}:{type:"text",text:t.text})}export{n as promptBlocksToAcpContent};
@@ -1,106 +1 @@
1
- import { STANDARD_AGENT_EVENT_TYPES, createAgentMessageChunk, createAgentProgressChunk, createAgentThoughtChunk, createToolCall, createToolCallUpdate, firstString, normalizeToolKind, normalizeToolStatus } from "../standard-events.js";
2
- export function acpSessionUpdateFromParams(params) {
3
- if (!params || typeof params !== "object" || Array.isArray(params))
4
- return null;
5
- const record = params;
6
- const update = record.update;
7
- if (!update || typeof update !== "object" || Array.isArray(update))
8
- return null;
9
- return {
10
- sessionId: typeof record.sessionId === "string" ? record.sessionId : undefined,
11
- update: update
12
- };
13
- }
14
- export function eventFromAcpSessionUpdate(params) {
15
- const parsed = acpSessionUpdateFromParams(params);
16
- if (!parsed)
17
- return null;
18
- const updateType = String(parsed.update.sessionUpdate || "");
19
- if (updateType === "agent_message_chunk") {
20
- return createAgentMessageChunk({ text: textFromAcpContent(parsed.update.content), method: "session/update", params });
21
- }
22
- if (updateType === "agent_progress_chunk") {
23
- return createAgentProgressChunk({ text: textFromAcpContent(parsed.update.content), method: "session/update", params });
24
- }
25
- if (updateType === "agent_thought_chunk") {
26
- return createAgentThoughtChunk({ text: textFromAcpContent(parsed.update.content), method: "session/update", params });
27
- }
28
- if (updateType === "tool_call" || updateType === "tool_call_update") {
29
- return toolEventFromAcpUpdate(updateType, parsed.update, params);
30
- }
31
- if (STANDARD_AGENT_EVENT_TYPES.has(updateType)) {
32
- return {
33
- type: updateType,
34
- label: labelForUpdate(updateType),
35
- ...(textFromAcpContent(parsed.update.content) ? { text: textFromAcpContent(parsed.update.content) } : {}),
36
- raw: { method: "session/update", params, update: parsed.update },
37
- atMs: Date.now()
38
- };
39
- }
40
- return null;
41
- }
42
- export function statusFromAcpSessionUpdate(params) {
43
- const parsed = acpSessionUpdateFromParams(params);
44
- if (!parsed)
45
- return null;
46
- const updateType = String(parsed.update.sessionUpdate || "");
47
- if (updateType === "agent_message_chunk")
48
- return { phase: "thinking", label: "回复中", detail: textPreview(textFromAcpContent(parsed.update.content)), updateType };
49
- if (updateType === "agent_progress_chunk")
50
- return { phase: "thinking", label: "处理中", detail: textPreview(textFromAcpContent(parsed.update.content)), updateType };
51
- if (updateType === "agent_thought_chunk")
52
- return { phase: "thinking", label: "思考中", detail: textPreview(textFromAcpContent(parsed.update.content)), updateType };
53
- if (updateType === "tool_call" || updateType === "tool_call_update")
54
- return { phase: "exploring", label: "执行工具", detail: firstString(parsed.update.title, parsed.update.kind), updateType };
55
- if (updateType === "plan" || updateType === "plan_update")
56
- return { phase: "thinking", label: "规划中", updateType };
57
- if (updateType === "aca_context_maintenance") {
58
- return {
59
- phase: "thinking",
60
- label: firstString(parsed.update.label) || "正在整理会话上下文",
61
- detail: firstString(parsed.update.detail),
62
- updateType
63
- };
64
- }
65
- return null;
66
- }
67
- export function textFromAcpContent(content) {
68
- if (typeof content === "string")
69
- return content;
70
- if (!content || typeof content !== "object" || Array.isArray(content))
71
- return "";
72
- const record = content;
73
- if (typeof record.text === "string")
74
- return record.text;
75
- return "";
76
- }
77
- function toolEventFromAcpUpdate(updateType, update, params) {
78
- const toolCallId = firstString(update.toolCallId, update.id) || `acp.${Date.now()}`;
79
- const kind = normalizeToolKind(firstString(update.kind, update.title));
80
- const status = normalizeToolStatus(firstString(update.status));
81
- const input = update.rawInput ?? update.input;
82
- const output = update.rawOutput ?? update.output;
83
- const createInput = {
84
- method: "session/update",
85
- params,
86
- toolCallId,
87
- kind,
88
- title: firstString(update.title, update.kind),
89
- status,
90
- rawInput: input,
91
- rawOutput: output,
92
- content: update.content,
93
- locations: Array.isArray(update.locations) ? update.locations.filter((item) => Boolean(item && typeof item === "object" && !Array.isArray(item))) : undefined,
94
- meta: update._meta && typeof update._meta === "object" && !Array.isArray(update._meta) ? update._meta : undefined
95
- };
96
- return updateType === "tool_call" ? createToolCall(createInput) : createToolCallUpdate(createInput);
97
- }
98
- function labelForUpdate(updateType) {
99
- return updateType.replace(/_/g, " ");
100
- }
101
- function textPreview(value) {
102
- const normalized = value.replace(/\s+/g, " ").trim();
103
- if (!normalized)
104
- return undefined;
105
- return normalized.length > 120 ? `${normalized.slice(0, 117)}...` : normalized;
106
- }
1
+ import{STANDARD_AGENT_EVENT_TYPES as g,createAgentMessageChunk as h,createAgentProgressChunk as _,createAgentThoughtChunk as y,createToolCall as A,createToolCallUpdate as k,firstString as o,normalizeToolKind as m,normalizeToolStatus as b}from"../standard-events.js";function l(n){if(!n||typeof n!="object"||Array.isArray(n))return null;const t=n,e=t.update;return!e||typeof e!="object"||Array.isArray(e)?null:{sessionId:typeof t.sessionId=="string"?t.sessionId:void 0,update:e}}function w(n){const t=l(n);if(!t)return null;const e=String(t.update.sessionUpdate||"");return e==="agent_message_chunk"?h({text:r(t.update.content),method:"session/update",params:n}):e==="agent_progress_chunk"?_({text:r(t.update.content),method:"session/update",params:n}):e==="agent_thought_chunk"?y({text:r(t.update.content),method:"session/update",params:n}):e==="tool_call"||e==="tool_call_update"?x(e,t.update,n):g.has(e)?{type:e,label:T(e),...r(t.update.content)?{text:r(t.update.content)}:{},raw:{method:"session/update",params:n,update:t.update},atMs:Date.now()}:null}function C(n){const t=l(n);if(!t)return null;const e=String(t.update.sessionUpdate||"");return e==="agent_message_chunk"?{phase:"thinking",label:"回复中",detail:i(r(t.update.content)),updateType:e}:e==="agent_progress_chunk"?{phase:"thinking",label:"处理中",detail:i(r(t.update.content)),updateType:e}:e==="agent_thought_chunk"?{phase:"thinking",label:"思考中",detail:i(r(t.update.content)),updateType:e}:e==="tool_call"||e==="tool_call_update"?{phase:"exploring",label:"执行工具",detail:o(t.update.title,t.update.kind),updateType:e}:e==="plan"||e==="plan_update"?{phase:"thinking",label:"规划中",updateType:e}:e==="aca_context_maintenance"?{phase:"thinking",label:o(t.update.label)||"正在整理会话上下文",detail:o(t.update.detail),updateType:e}:null}function r(n){if(typeof n=="string")return n;if(!n||typeof n!="object"||Array.isArray(n))return"";const t=n;return typeof t.text=="string"?t.text:""}function x(n,t,e){const u=o(t.toolCallId,t.id)||`acp.${Date.now()}`,c=m(o(t.kind,t.title)),p=b(o(t.status)),d=t.rawInput??t.input,f=t.rawOutput??t.output,s={method:"session/update",params:e,toolCallId:u,kind:c,title:o(t.title,t.kind),status:p,rawInput:d,rawOutput:f,content:t.content,locations:Array.isArray(t.locations)?t.locations.filter(a=>!!(a&&typeof a=="object"&&!Array.isArray(a))):void 0,meta:t._meta&&typeof t._meta=="object"&&!Array.isArray(t._meta)?t._meta:void 0};return n==="tool_call"?A(s):k(s)}function T(n){return n.replace(/_/g," ")}function i(n){const t=n.replace(/\s+/g," ").trim();if(t)return t.length>120?`${t.slice(0,117)}...`:t}export{l as acpSessionUpdateFromParams,w as eventFromAcpSessionUpdate,C as statusFromAcpSessionUpdate,r as textFromAcpContent};
@@ -1,34 +1 @@
1
- import { spawn } from "node:child_process";
2
- import { acpChildEnvironment } from "../../core/process-identity.js";
3
- export function startAcpProcess(command, args, cwd) {
4
- const child = spawn(command, args, {
5
- cwd,
6
- stdio: ["pipe", "pipe", "pipe"],
7
- env: acpChildEnvironment()
8
- });
9
- return { command, args, child };
10
- }
11
- export async function closeAcpProcess(child) {
12
- if (child.exitCode !== null || child.signalCode !== null)
13
- return;
14
- child.stdin.end();
15
- if (await waitForProcessClose(child, 2_000))
16
- return;
17
- child.kill("SIGTERM");
18
- if (await waitForProcessClose(child, 1_000))
19
- return;
20
- child.kill("SIGKILL");
21
- await waitForProcessClose(child, 1_000);
22
- }
23
- function waitForProcessClose(child, timeoutMs) {
24
- if (child.exitCode !== null || child.signalCode !== null)
25
- return Promise.resolve(true);
26
- return new Promise((resolve) => {
27
- const timer = setTimeout(() => resolve(false), timeoutMs);
28
- timer.unref();
29
- child.once("close", () => {
30
- clearTimeout(timer);
31
- resolve(true);
32
- });
33
- });
34
- }
1
+ import{spawn as i}from"node:child_process";import{acpChildEnvironment as s}from"../../core/process-identity.js";function a(e,t,n){const r=i(e,t,{cwd:n,stdio:["pipe","pipe","pipe"],env:s()});return{command:e,args:t,child:r}}async function l(e){e.exitCode!==null||e.signalCode!==null||(e.stdin.end(),!await o(e,2e3)&&(e.kill("SIGTERM"),!await o(e,1e3)&&(e.kill("SIGKILL"),await o(e,1e3))))}function o(e,t){return e.exitCode!==null||e.signalCode!==null?Promise.resolve(!0):new Promise(n=>{const r=setTimeout(()=>n(!1),t);r.unref(),e.once("close",()=>{clearTimeout(r),n(!0)})})}export{l as closeAcpProcess,a as startAcpProcess};
@@ -1,248 +1 @@
1
- import fs from "node:fs";
2
- import { randomUUID } from "node:crypto";
3
- import { AcpClient } from "./acp-client.js";
4
- import { closeAcpProcess, startAcpProcess } from "./acp-process.js";
5
- const DEFAULT_IDLE_TTL_MS = 5 * 60 * 1000;
6
- const DEFAULT_SWEEP_INTERVAL_MS = 60 * 1000;
7
- const DEFAULT_MAX_IDLE_RUNTIMES = 4;
8
- export class AcpRuntimePool {
9
- runtimes = new Map();
10
- idleTtlMs;
11
- sweepIntervalMs;
12
- maxIdleRuntimes;
13
- sweepTimer = null;
14
- disposeAllPromise = null;
15
- constructor(options = {}) {
16
- this.idleTtlMs = Math.max(1_000, options.idleTtlMs ?? readRuntimePoolIdleTtlMs());
17
- this.sweepIntervalMs = Math.max(1_000, options.sweepIntervalMs ?? readRuntimePoolSweepIntervalMs());
18
- this.maxIdleRuntimes = Math.max(0, options.maxIdleRuntimes ?? readRuntimePoolMaxIdleRuntimes());
19
- }
20
- async acquire(input) {
21
- if (this.disposeAllPromise)
22
- await this.disposeAllPromise;
23
- const cwd = fs.realpathSync(input.cwd);
24
- this.ensureSweep();
25
- const existingKey = input.providerSessionId ? runtimeKey(input.providerId, cwd, input.providerSessionId) : null;
26
- const existing = existingKey ? this.runtimes.get(existingKey) : null;
27
- if (existing && !this.isRuntimeAlive(existing)) {
28
- await this.disposeEntry(existing).catch(() => void 0);
29
- }
30
- const reusable = existingKey ? this.runtimes.get(existingKey) : null;
31
- if (reusable) {
32
- this.activateEntry(reusable, input);
33
- return this.createLease(reusable);
34
- }
35
- const tempSessionId = input.providerSessionId || `pending:${randomUUID()}`;
36
- const key = runtimeKey(input.providerId, cwd, tempSessionId);
37
- const entry = this.createEntry({ ...input, cwd, key, providerSessionId: input.providerSessionId ?? null });
38
- this.runtimes.set(key, entry);
39
- return this.createLease(entry);
40
- }
41
- disposeAll() {
42
- if (this.disposeAllPromise)
43
- return this.disposeAllPromise;
44
- const promise = this.performDisposeAll();
45
- this.disposeAllPromise = promise;
46
- void promise.finally(() => {
47
- if (this.disposeAllPromise === promise)
48
- this.disposeAllPromise = null;
49
- });
50
- return promise;
51
- }
52
- async performDisposeAll() {
53
- if (this.sweepTimer) {
54
- clearInterval(this.sweepTimer);
55
- this.sweepTimer = null;
56
- }
57
- const entries = [...this.runtimes.values()];
58
- await Promise.all(entries.map((entry) => this.disposeEntry(entry).catch(() => void 0)));
59
- }
60
- async sweepIdle(nowMs = Date.now()) {
61
- const entries = [...this.runtimes.values()];
62
- for (const entry of entries) {
63
- if (entry.inUse || entry.disposed)
64
- continue;
65
- if (nowMs - entry.lastUsedAtMs < this.idleTtlMs)
66
- continue;
67
- await this.disposeEntry(entry).catch(() => void 0);
68
- }
69
- await this.trimIdleRuntimes();
70
- }
71
- stats() {
72
- const entries = [...this.runtimes.values()].filter((entry) => !entry.disposed);
73
- return {
74
- total: entries.length,
75
- inUse: entries.filter((entry) => entry.inUse).length,
76
- idle: entries.filter((entry) => !entry.inUse).length,
77
- pids: entries.map((entry) => entry.started.child.pid).filter((pid) => Number.isInteger(pid))
78
- };
79
- }
80
- createEntry(input) {
81
- const started = startAcpProcess(input.command, input.args, input.cwd);
82
- const entry = {
83
- key: input.key,
84
- providerId: input.providerId,
85
- cwd: input.cwd,
86
- providerSessionId: input.providerSessionId,
87
- started,
88
- client: null,
89
- initialized: false,
90
- sessionEstablished: false,
91
- inUse: true,
92
- disposed: false,
93
- disposePromise: null,
94
- lastUsedAtMs: Date.now(),
95
- sessionResponse: null,
96
- activeHandlers: {
97
- onSessionUpdate: input.onSessionUpdate,
98
- onClientRequest: input.onClientRequest
99
- }
100
- };
101
- entry.client = new AcpClient(started.child, (params, message) => entry.activeHandlers?.onSessionUpdate(params, message), async (method, params, message) => {
102
- if (!entry.activeHandlers?.onClientRequest)
103
- throw new Error(`Unsupported ACP client request: ${method}`);
104
- return entry.activeHandlers.onClientRequest(method, params, message);
105
- });
106
- started.child.once("close", () => {
107
- if (this.runtimes.get(entry.key) === entry)
108
- this.runtimes.delete(entry.key);
109
- entry.disposed = true;
110
- entry.inUse = false;
111
- entry.activeHandlers = null;
112
- });
113
- return entry;
114
- }
115
- activateEntry(entry, input) {
116
- if (entry.inUse) {
117
- throw new Error(`ACP runtime is already in use: provider=${entry.providerId} session=${entry.providerSessionId ?? "unknown"}`);
118
- }
119
- entry.inUse = true;
120
- entry.activeHandlers = {
121
- onSessionUpdate: input.onSessionUpdate,
122
- onClientRequest: input.onClientRequest
123
- };
124
- }
125
- createLease(entry) {
126
- let released = false;
127
- return {
128
- get client() {
129
- return entry.client;
130
- },
131
- get started() {
132
- return entry.started;
133
- },
134
- get cwd() {
135
- return entry.cwd;
136
- },
137
- get providerSessionId() {
138
- return entry.providerSessionId;
139
- },
140
- get sessionResponse() {
141
- return entry.sessionResponse;
142
- },
143
- get initialized() {
144
- return entry.initialized;
145
- },
146
- get sessionEstablished() {
147
- return entry.sessionEstablished;
148
- },
149
- markInitialized: () => {
150
- entry.initialized = true;
151
- },
152
- setProviderSession: (input) => {
153
- entry.providerSessionId = input.providerSessionId;
154
- entry.sessionEstablished = true;
155
- if ("sessionResponse" in input)
156
- entry.sessionResponse = input.sessionResponse ?? null;
157
- this.rekeyEntry(entry, runtimeKey(entry.providerId, entry.cwd, input.providerSessionId));
158
- },
159
- setSessionResponse: (sessionResponse) => {
160
- entry.sessionResponse = sessionResponse;
161
- },
162
- release: () => {
163
- if (released)
164
- return;
165
- released = true;
166
- entry.inUse = false;
167
- entry.activeHandlers = null;
168
- entry.lastUsedAtMs = Date.now();
169
- void this.trimIdleRuntimes();
170
- },
171
- dispose: async () => {
172
- released = true;
173
- await this.disposeEntry(entry);
174
- }
175
- };
176
- }
177
- rekeyEntry(entry, nextKey) {
178
- if (entry.key === nextKey)
179
- return;
180
- const previous = this.runtimes.get(nextKey);
181
- if (previous && previous !== entry) {
182
- if (previous.inUse) {
183
- throw new Error(`ACP runtime key is already active: provider=${previous.providerId} session=${previous.providerSessionId ?? "unknown"}`);
184
- }
185
- void this.disposeEntry(previous);
186
- }
187
- if (this.runtimes.get(entry.key) === entry)
188
- this.runtimes.delete(entry.key);
189
- entry.key = nextKey;
190
- this.runtimes.set(nextKey, entry);
191
- }
192
- async disposeEntry(entry) {
193
- if (entry.disposePromise)
194
- return entry.disposePromise;
195
- if (entry.disposed)
196
- return;
197
- const promise = this.performDisposeEntry(entry);
198
- entry.disposePromise = promise;
199
- return promise;
200
- }
201
- async performDisposeEntry(entry) {
202
- entry.disposed = true;
203
- entry.inUse = false;
204
- entry.activeHandlers = null;
205
- if (this.runtimes.get(entry.key) === entry)
206
- this.runtimes.delete(entry.key);
207
- if (entry.providerSessionId) {
208
- await entry.client.closeSession(entry.providerSessionId, 5_000).catch(() => void 0);
209
- }
210
- await closeAcpProcess(entry.started.child);
211
- }
212
- async trimIdleRuntimes() {
213
- const idle = [...this.runtimes.values()]
214
- .filter((entry) => !entry.inUse && !entry.disposed)
215
- .sort((left, right) => left.lastUsedAtMs - right.lastUsedAtMs);
216
- const excess = Math.max(0, idle.length - this.maxIdleRuntimes);
217
- for (const entry of idle.slice(0, excess)) {
218
- await this.disposeEntry(entry).catch(() => void 0);
219
- }
220
- }
221
- isRuntimeAlive(entry) {
222
- return !entry.disposed && entry.started.child.exitCode === null && entry.started.child.signalCode === null;
223
- }
224
- ensureSweep() {
225
- if (this.sweepTimer)
226
- return;
227
- this.sweepTimer = setInterval(() => {
228
- void this.sweepIdle();
229
- }, this.sweepIntervalMs);
230
- this.sweepTimer.unref();
231
- }
232
- }
233
- export const defaultAcpRuntimePool = new AcpRuntimePool();
234
- function runtimeKey(providerId, cwd, providerSessionId) {
235
- return `${providerId}\0${cwd}\0${providerSessionId}`;
236
- }
237
- function readRuntimePoolIdleTtlMs() {
238
- const parsed = Number.parseInt(process.env.ACA_ACP_RUNTIME_IDLE_TTL_MS ?? "", 10);
239
- return Number.isInteger(parsed) && parsed >= 1_000 ? parsed : DEFAULT_IDLE_TTL_MS;
240
- }
241
- function readRuntimePoolSweepIntervalMs() {
242
- const parsed = Number.parseInt(process.env.ACA_ACP_RUNTIME_SWEEP_INTERVAL_MS ?? "", 10);
243
- return Number.isInteger(parsed) && parsed >= 1_000 ? parsed : DEFAULT_SWEEP_INTERVAL_MS;
244
- }
245
- function readRuntimePoolMaxIdleRuntimes() {
246
- const parsed = Number.parseInt(process.env.ACA_ACP_RUNTIME_MAX_IDLE ?? "", 10);
247
- return Number.isInteger(parsed) && parsed >= 0 ? Math.min(parsed, 32) : DEFAULT_MAX_IDLE_RUNTIMES;
248
- }
1
+ import c from"node:fs";import{randomUUID as p}from"node:crypto";import{AcpClient as u}from"./acp-client.js";import{closeAcpProcess as m,startAcpProcess as h}from"./acp-process.js";const v=300*1e3,I=60*1e3,f=4;class w{runtimes=new Map;idleTtlMs;sweepIntervalMs;maxIdleRuntimes;sweepTimer=null;disposeAllPromise=null;constructor(e={}){this.idleTtlMs=Math.max(1e3,e.idleTtlMs??A()),this.sweepIntervalMs=Math.max(1e3,e.sweepIntervalMs??E()),this.maxIdleRuntimes=Math.max(0,e.maxIdleRuntimes??S())}async acquire(e){this.disposeAllPromise&&await this.disposeAllPromise;const i=c.realpathSync(e.cwd);this.ensureSweep();const s=e.providerSessionId?d(e.providerId,i,e.providerSessionId):null,r=s?this.runtimes.get(s):null;r&&!this.isRuntimeAlive(r)&&await this.disposeEntry(r).catch(()=>{});const n=s?this.runtimes.get(s):null;if(n)return this.activateEntry(n,e),this.createLease(n);const o=e.providerSessionId||`pending:${p()}`,l=d(e.providerId,i,o),a=this.createEntry({...e,cwd:i,key:l,providerSessionId:e.providerSessionId??null});return this.runtimes.set(l,a),this.createLease(a)}disposeAll(){if(this.disposeAllPromise)return this.disposeAllPromise;const e=this.performDisposeAll();return this.disposeAllPromise=e,e.finally(()=>{this.disposeAllPromise===e&&(this.disposeAllPromise=null)}),e}async performDisposeAll(){this.sweepTimer&&(clearInterval(this.sweepTimer),this.sweepTimer=null);const e=[...this.runtimes.values()];await Promise.all(e.map(i=>this.disposeEntry(i).catch(()=>{})))}async sweepIdle(e=Date.now()){const i=[...this.runtimes.values()];for(const s of i)s.inUse||s.disposed||e-s.lastUsedAtMs<this.idleTtlMs||await this.disposeEntry(s).catch(()=>{});await this.trimIdleRuntimes()}stats(){const e=[...this.runtimes.values()].filter(i=>!i.disposed);return{total:e.length,inUse:e.filter(i=>i.inUse).length,idle:e.filter(i=>!i.inUse).length,pids:e.map(i=>i.started.child.pid).filter(i=>Number.isInteger(i))}}createEntry(e){const i=h(e.command,e.args,e.cwd),s={key:e.key,providerId:e.providerId,cwd:e.cwd,providerSessionId:e.providerSessionId,started:i,client:null,initialized:!1,sessionEstablished:!1,inUse:!0,disposed:!1,disposePromise:null,lastUsedAtMs:Date.now(),sessionResponse:null,activeHandlers:{onSessionUpdate:e.onSessionUpdate,onClientRequest:e.onClientRequest}};return s.client=new u(i.child,(r,n)=>s.activeHandlers?.onSessionUpdate(r,n),async(r,n,o)=>{if(!s.activeHandlers?.onClientRequest)throw new Error(`Unsupported ACP client request: ${r}`);return s.activeHandlers.onClientRequest(r,n,o)}),i.child.once("close",()=>{this.runtimes.get(s.key)===s&&this.runtimes.delete(s.key),s.disposed=!0,s.inUse=!1,s.activeHandlers=null}),s}activateEntry(e,i){if(e.inUse)throw new Error(`ACP runtime is already in use: provider=${e.providerId} session=${e.providerSessionId??"unknown"}`);e.inUse=!0,e.activeHandlers={onSessionUpdate:i.onSessionUpdate,onClientRequest:i.onClientRequest}}createLease(e){let i=!1;return{get client(){return e.client},get started(){return e.started},get cwd(){return e.cwd},get providerSessionId(){return e.providerSessionId},get sessionResponse(){return e.sessionResponse},get initialized(){return e.initialized},get sessionEstablished(){return e.sessionEstablished},markInitialized:()=>{e.initialized=!0},setProviderSession:s=>{e.providerSessionId=s.providerSessionId,e.sessionEstablished=!0,"sessionResponse"in s&&(e.sessionResponse=s.sessionResponse??null),this.rekeyEntry(e,d(e.providerId,e.cwd,s.providerSessionId))},setSessionResponse:s=>{e.sessionResponse=s},release:()=>{i||(i=!0,e.inUse=!1,e.activeHandlers=null,e.lastUsedAtMs=Date.now(),this.trimIdleRuntimes())},dispose:async()=>{i=!0,await this.disposeEntry(e)}}}rekeyEntry(e,i){if(e.key===i)return;const s=this.runtimes.get(i);if(s&&s!==e){if(s.inUse)throw new Error(`ACP runtime key is already active: provider=${s.providerId} session=${s.providerSessionId??"unknown"}`);this.disposeEntry(s)}this.runtimes.get(e.key)===e&&this.runtimes.delete(e.key),e.key=i,this.runtimes.set(i,e)}async disposeEntry(e){if(e.disposePromise)return e.disposePromise;if(e.disposed)return;const i=this.performDisposeEntry(e);return e.disposePromise=i,i}async performDisposeEntry(e){e.disposed=!0,e.inUse=!1,e.activeHandlers=null,this.runtimes.get(e.key)===e&&this.runtimes.delete(e.key),e.providerSessionId&&await e.client.closeSession(e.providerSessionId,5e3).catch(()=>{}),await m(e.started.child)}async trimIdleRuntimes(){const e=[...this.runtimes.values()].filter(s=>!s.inUse&&!s.disposed).sort((s,r)=>s.lastUsedAtMs-r.lastUsedAtMs),i=Math.max(0,e.length-this.maxIdleRuntimes);for(const s of e.slice(0,i))await this.disposeEntry(s).catch(()=>{})}isRuntimeAlive(e){return!e.disposed&&e.started.child.exitCode===null&&e.started.child.signalCode===null}ensureSweep(){this.sweepTimer||(this.sweepTimer=setInterval(()=>{this.sweepIdle()},this.sweepIntervalMs),this.sweepTimer.unref())}}const _=new w;function d(t,e,i){return`${t}\0${e}\0${i}`}function A(){const t=Number.parseInt(process.env.ACA_ACP_RUNTIME_IDLE_TTL_MS??"",10);return Number.isInteger(t)&&t>=1e3?t:v}function E(){const t=Number.parseInt(process.env.ACA_ACP_RUNTIME_SWEEP_INTERVAL_MS??"",10);return Number.isInteger(t)&&t>=1e3?t:I}function S(){const t=Number.parseInt(process.env.ACA_ACP_RUNTIME_MAX_IDLE??"",10);return Number.isInteger(t)&&t>=0?Math.min(t,32):f}export{w as AcpRuntimePool,_ as defaultAcpRuntimePool};
@@ -1,29 +1 @@
1
- import { acpSessionUpdateFromParams } from "./acp-events.js";
2
- /**
3
- * Converts the standard ACP UsageUpdate shape into ACA's persisted session
4
- * usage shape. ACP defines `used` as tokens currently in context and `size`
5
- * as the model context-window size. Do not derive this from message length.
6
- */
7
- export function contextUsageFromAcpSessionUpdate(params, fallbackContextWindow) {
8
- const parsed = acpSessionUpdateFromParams(params);
9
- if (!parsed || parsed.update.sessionUpdate !== "usage_update")
10
- return null;
11
- const usedTokens = nonNegativeNumber(parsed.update.used);
12
- const reportedWindow = nonNegativeNumber(parsed.update.size);
13
- const contextWindow = reportedWindow || nonNegativeNumber(fallbackContextWindow);
14
- if (usedTokens <= 0 || contextWindow <= 0)
15
- return null;
16
- return {
17
- usedTokens,
18
- // ACP's standard update intentionally has no input-token breakdown.
19
- inputTokens: usedTokens,
20
- contextWindow,
21
- ratio: Math.min(1, usedTokens / contextWindow),
22
- observedAtMs: Date.now(),
23
- state: "active",
24
- maintenanceAction: null
25
- };
26
- }
27
- function nonNegativeNumber(value) {
28
- return typeof value === "number" && Number.isFinite(value) && value >= 0 ? value : 0;
29
- }
1
+ import{acpSessionUpdateFromParams as i}from"./acp-events.js";function d(e,s){const t=i(e);if(!t||t.update.sessionUpdate!=="usage_update")return null;const n=r(t.update.used),o=r(t.update.size)||r(s);return n<=0||o<=0?null:{usedTokens:n,inputTokens:n,contextWindow:o,ratio:Math.min(1,n/o),observedAtMs:Date.now(),state:"active",maintenanceAction:null}}function r(e){return typeof e=="number"&&Number.isFinite(e)&&e>=0?e:0}export{d as contextUsageFromAcpSessionUpdate};