@automatalabs/acp-agents 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +202 -0
- package/dist/acp-client.d.ts +164 -0
- package/dist/acp-client.d.ts.map +1 -0
- package/dist/acp-client.js +611 -0
- package/dist/backend.d.ts +28 -0
- package/dist/backend.d.ts.map +1 -0
- package/dist/backend.js +4 -0
- package/dist/backends/claude.d.ts +10 -0
- package/dist/backends/claude.d.ts.map +1 -0
- package/dist/backends/claude.js +48 -0
- package/dist/backends/codex.d.ts +10 -0
- package/dist/backends/codex.d.ts.map +1 -0
- package/dist/backends/codex.js +62 -0
- package/dist/errors-map.d.ts +5 -0
- package/dist/errors-map.d.ts.map +1 -0
- package/dist/errors-map.js +47 -0
- package/dist/index.d.ts +16 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +16 -0
- package/dist/permissions.d.ts +10 -0
- package/dist/permissions.d.ts.map +1 -0
- package/dist/permissions.js +86 -0
- package/dist/pool.d.ts +35 -0
- package/dist/pool.d.ts.map +1 -0
- package/dist/pool.js +103 -0
- package/dist/runner.d.ts +22 -0
- package/dist/runner.d.ts.map +1 -0
- package/dist/runner.js +211 -0
- package/dist/schema-strict.d.ts +11 -0
- package/dist/schema-strict.d.ts.map +1 -0
- package/dist/schema-strict.js +189 -0
- package/dist/structured-output.d.ts +37 -0
- package/dist/structured-output.d.ts.map +1 -0
- package/dist/structured-output.js +88 -0
- package/dist/usage.d.ts +21 -0
- package/dist/usage.d.ts.map +1 -0
- package/dist/usage.js +52 -0
- package/package.json +33 -0
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
// ClaudeBackend — drives @agentclientprotocol/claude-agent-acp@0.53.0 (over the Claude
|
|
2
|
+
// Agent SDK). Structured output rides the vendor `_meta.claudeCode` channel at session/new:
|
|
3
|
+
// options.outputFormat = { type:"json_schema", schema } // the SDK's native constraint
|
|
4
|
+
// emitRawSDKMessages = true // MANDATORY to READ the result
|
|
5
|
+
// The parsed object lands on SDKResultSuccess.structured_output, observable ONLY off the raw
|
|
6
|
+
// `_claude/sdkMessage` extension notification (the runner's ACP client captures it).
|
|
7
|
+
import { createRequire } from "node:module";
|
|
8
|
+
import { splitArgs } from "../backend.js";
|
|
9
|
+
import { toJsonSchema } from "../schema-strict.js";
|
|
10
|
+
const require = createRequire(import.meta.url);
|
|
11
|
+
export class ClaudeBackend {
|
|
12
|
+
id = "claude";
|
|
13
|
+
spawnConfig() {
|
|
14
|
+
const env = process.env;
|
|
15
|
+
const override = env.AGENTPRISM_CLAUDE_ACP_CMD;
|
|
16
|
+
if (override) {
|
|
17
|
+
return { command: override, args: splitArgs(env.AGENTPRISM_CLAUDE_ACP_ARGS), env };
|
|
18
|
+
}
|
|
19
|
+
// Prefer the installed package's bin script run under the current node; fall back to npx
|
|
20
|
+
// when it is not resolvable from this install.
|
|
21
|
+
try {
|
|
22
|
+
const bin = require.resolve("@agentclientprotocol/claude-agent-acp/dist/index.js");
|
|
23
|
+
return { command: process.execPath, args: [bin], env };
|
|
24
|
+
}
|
|
25
|
+
catch {
|
|
26
|
+
return { command: "npx", args: ["-y", "@agentclientprotocol/claude-agent-acp"], env };
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
sessionMeta(schema) {
|
|
30
|
+
if (!schema)
|
|
31
|
+
return undefined;
|
|
32
|
+
return {
|
|
33
|
+
claudeCode: {
|
|
34
|
+
options: {
|
|
35
|
+
outputFormat: { type: "json_schema", schema: toJsonSchema(schema) },
|
|
36
|
+
},
|
|
37
|
+
emitRawSDKMessages: true,
|
|
38
|
+
},
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
promptMeta() {
|
|
42
|
+
// Claude's schema is session-scoped (read at session/new); nothing on the turn.
|
|
43
|
+
return undefined;
|
|
44
|
+
}
|
|
45
|
+
nativeStructured(source) {
|
|
46
|
+
return source.rawStructuredOutput();
|
|
47
|
+
}
|
|
48
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { TSchema } from "typebox";
|
|
2
|
+
import type { Backend, SpawnConfig, StructuredSource } from "../backend.js";
|
|
3
|
+
export declare class CodexBackend implements Backend {
|
|
4
|
+
readonly id: "codex";
|
|
5
|
+
spawnConfig(): SpawnConfig;
|
|
6
|
+
sessionMeta(): Record<string, unknown> | undefined;
|
|
7
|
+
promptMeta(schema: TSchema | undefined): Record<string, unknown> | undefined;
|
|
8
|
+
nativeStructured(source: StructuredSource): unknown;
|
|
9
|
+
}
|
|
10
|
+
//# sourceMappingURL=codex.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"codex.d.ts","sourceRoot":"","sources":["../../src/backends/codex.ts"],"names":[],"mappings":"AASA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,SAAS,CAAC;AAEvC,OAAO,KAAK,EAAE,OAAO,EAAE,WAAW,EAAE,gBAAgB,EAAE,MAAM,eAAe,CAAC;AAO5E,qBAAa,YAAa,YAAW,OAAO;IAC1C,QAAQ,CAAC,EAAE,EAAG,OAAO,CAAU;IAE/B,WAAW,IAAI,WAAW;IAe1B,WAAW,IAAI,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,SAAS;IAKlD,UAAU,CAAC,MAAM,EAAE,OAAO,GAAG,SAAS,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,SAAS;IAK5E,gBAAgB,CAAC,MAAM,EAAE,gBAAgB,GAAG,OAAO;CAoBpD"}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
// CodexBackend — drives the installed npm dep @automatalabs/codex-acp, a published fork of
|
|
2
|
+
// @agentclientprotocol/codex-acp that bakes in the outputSchema patch. The patch forwards
|
|
3
|
+
// request._meta["agentprism/outputSchema"] into the Codex App Server's turn/start.outputSchema,
|
|
4
|
+
// which the shipped @openai/codex binary honors end-to-end as an OpenAI Responses-API STRICT
|
|
5
|
+
// constraint on the final assistant message. So the schema rides per-PROMPT `_meta` (not
|
|
6
|
+
// session/new), normalized to OpenAI strict rules first. Output needs no special channel: the
|
|
7
|
+
// constrained final message flows back over the normal agent-message stream, so the backend
|
|
8
|
+
// reads the final text and JSON.parses it.
|
|
9
|
+
import { createRequire } from "node:module";
|
|
10
|
+
import { META_KEYS } from "@automatalabs/shared-types";
|
|
11
|
+
import { splitArgs } from "../backend.js";
|
|
12
|
+
import { toStrictJsonSchema } from "../schema-strict.js";
|
|
13
|
+
import { findJsonBlock } from "../structured-output.js";
|
|
14
|
+
const require = createRequire(import.meta.url);
|
|
15
|
+
export class CodexBackend {
|
|
16
|
+
id = "codex";
|
|
17
|
+
spawnConfig() {
|
|
18
|
+
const env = process.env;
|
|
19
|
+
const override = env.AGENTPRISM_CODEX_ACP_CMD;
|
|
20
|
+
if (override) {
|
|
21
|
+
return { command: override, args: splitArgs(env.AGENTPRISM_CODEX_ACP_ARGS), env };
|
|
22
|
+
}
|
|
23
|
+
// Run the installed codex-acp under the current node. AGENTPRISM_CODEX_ACP_BIN overrides the
|
|
24
|
+
// resolved path; otherwise resolve the package's main (dist/index.js) from node_modules so it
|
|
25
|
+
// ships on a clean `git clone && pnpm install` (the @automatalabs/codex-acp fork already bakes
|
|
26
|
+
// in the outputSchema patch). Works from both src/ and the compiled dist/.
|
|
27
|
+
const bin = env.AGENTPRISM_CODEX_ACP_BIN ?? require.resolve("@automatalabs/codex-acp");
|
|
28
|
+
return { command: process.execPath, args: [bin], env };
|
|
29
|
+
}
|
|
30
|
+
sessionMeta() {
|
|
31
|
+
// Codex carries the schema on the turn, not session/new.
|
|
32
|
+
return undefined;
|
|
33
|
+
}
|
|
34
|
+
promptMeta(schema) {
|
|
35
|
+
if (!schema)
|
|
36
|
+
return undefined;
|
|
37
|
+
return { [META_KEYS.outputSchema]: toStrictJsonSchema(schema) };
|
|
38
|
+
}
|
|
39
|
+
nativeStructured(source) {
|
|
40
|
+
const text = source.currentTurnText().trim();
|
|
41
|
+
if (!text)
|
|
42
|
+
return undefined;
|
|
43
|
+
// The constrained final message is pure JSON; parse it directly, then fall back to a
|
|
44
|
+
// balanced-block extraction if the turn also emitted leading prose.
|
|
45
|
+
try {
|
|
46
|
+
return JSON.parse(text);
|
|
47
|
+
}
|
|
48
|
+
catch {
|
|
49
|
+
// fall through to block extraction
|
|
50
|
+
}
|
|
51
|
+
const block = findJsonBlock(text);
|
|
52
|
+
if (block !== undefined) {
|
|
53
|
+
try {
|
|
54
|
+
return JSON.parse(block);
|
|
55
|
+
}
|
|
56
|
+
catch {
|
|
57
|
+
// give up; the runner's ladder will re-prompt / extract.
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
return undefined;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import { WorkflowError } from "@automatalabs/shared-types";
|
|
2
|
+
export declare function errorText(error: unknown): string;
|
|
3
|
+
/** Map any thrown error from the run path onto a seam-level WorkflowError. */
|
|
4
|
+
export declare function mapThrownError(error: unknown, label?: string): WorkflowError;
|
|
5
|
+
//# sourceMappingURL=errors-map.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"errors-map.d.ts","sourceRoot":"","sources":["../src/errors-map.ts"],"names":[],"mappings":"AAYA,OAAO,EAA0C,aAAa,EAAqB,MAAM,4BAA4B,CAAC;AAEtH,wBAAgB,SAAS,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,CAahD;AAED,8EAA8E;AAC9E,wBAAgB,cAAc,CAAC,KAAK,EAAE,OAAO,EAAE,KAAK,CAAC,EAAE,MAAM,GAAG,aAAa,CAmB5E"}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
// ACP failure -> WorkflowError{code, recoverable, resetHint}.
|
|
2
|
+
//
|
|
3
|
+
// Every hard backend failure REJECTS the ACP request (claude-agent-acp `failActive(...)`,
|
|
4
|
+
// codex-acp request errors), so the runner catches one thrown error and classifies it HERE.
|
|
5
|
+
// Provider usage/quota/rate walls are detected by running the thrown error's MESSAGE through
|
|
6
|
+
// classifyProviderLimit (shared-types) — this is the "gate on the error channel, never task
|
|
7
|
+
// text" rule: we only ever classify text that arrived via an error/reject, never the
|
|
8
|
+
// assistant's normal output. A matched wall becomes PROVIDER_USAGE_LIMIT (non-recoverable +
|
|
9
|
+
// resetHint -> the engine PAUSES and resumes instead of retrying into the same wall).
|
|
10
|
+
// Everything else is a recoverable AGENT_EXECUTION_ERROR (transient process/ACP faults that
|
|
11
|
+
// the engine retries). WorkflowErrors raised inside the ladder (SCHEMA_NONCOMPLIANCE,
|
|
12
|
+
// AGENT_EMPTY_OUTPUT) pass through unchanged.
|
|
13
|
+
import { classifyProviderLimit, isWorkflowError, WorkflowError, WorkflowErrorCode } from "@automatalabs/shared-types";
|
|
14
|
+
export function errorText(error) {
|
|
15
|
+
if (error instanceof Error)
|
|
16
|
+
return error.message;
|
|
17
|
+
if (typeof error === "string")
|
|
18
|
+
return error;
|
|
19
|
+
if (error && typeof error === "object") {
|
|
20
|
+
const message = error.message;
|
|
21
|
+
if (typeof message === "string")
|
|
22
|
+
return message;
|
|
23
|
+
try {
|
|
24
|
+
return JSON.stringify(error);
|
|
25
|
+
}
|
|
26
|
+
catch {
|
|
27
|
+
return String(error);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
return String(error);
|
|
31
|
+
}
|
|
32
|
+
/** Map any thrown error from the run path onto a seam-level WorkflowError. */
|
|
33
|
+
export function mapThrownError(error, label) {
|
|
34
|
+
// Already a seam error (e.g. SCHEMA_NONCOMPLIANCE / AGENT_EMPTY_OUTPUT raised in-band).
|
|
35
|
+
if (isWorkflowError(error))
|
|
36
|
+
return error;
|
|
37
|
+
const message = errorText(error);
|
|
38
|
+
const { matched, resetHint } = classifyProviderLimit(message);
|
|
39
|
+
if (matched) {
|
|
40
|
+
return new WorkflowError(message || "Provider usage/quota limit reached", WorkflowErrorCode.PROVIDER_USAGE_LIMIT, { recoverable: false, agentLabel: label, resetHint, details: error });
|
|
41
|
+
}
|
|
42
|
+
return new WorkflowError(message || "Subagent execution failed", WorkflowErrorCode.AGENT_EXECUTION_ERROR, {
|
|
43
|
+
recoverable: true,
|
|
44
|
+
agentLabel: label,
|
|
45
|
+
details: error,
|
|
46
|
+
});
|
|
47
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export { AcpAgentRunner, createAcpRunner, selectBackend } from "./runner.js";
|
|
2
|
+
export { PooledConnection, SessionHandle } from "./acp-client.js";
|
|
3
|
+
export type { AcpSessionOptions, PooledConnectionDeps } from "./acp-client.js";
|
|
4
|
+
export { AcpAgentPool, resolvePoolSize } from "./pool.js";
|
|
5
|
+
export type { AcpPoolOptions } from "./pool.js";
|
|
6
|
+
export type { Backend, BackendId, SpawnConfig, StructuredSource } from "./backend.js";
|
|
7
|
+
export { ClaudeBackend } from "./backends/claude.js";
|
|
8
|
+
export { CodexBackend } from "./backends/codex.js";
|
|
9
|
+
export { decidePermission } from "./permissions.js";
|
|
10
|
+
export type { ToolPolicy } from "./permissions.js";
|
|
11
|
+
export { UsageAccumulator } from "./usage.js";
|
|
12
|
+
export { toJsonSchema, toStrictJsonSchema } from "./schema-strict.js";
|
|
13
|
+
export { extractValidated, findJsonBlock, resolveStructuredOutput, validateValue, } from "./structured-output.js";
|
|
14
|
+
export type { ResolveOptions, StructuredSession } from "./structured-output.js";
|
|
15
|
+
export { errorText, mapThrownError } from "./errors-map.js";
|
|
16
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAMA,OAAO,EAAE,cAAc,EAAE,eAAe,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAE7E,OAAO,EAAE,gBAAgB,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC;AAClE,YAAY,EAAE,iBAAiB,EAAE,oBAAoB,EAAE,MAAM,iBAAiB,CAAC;AAC/E,OAAO,EAAE,YAAY,EAAE,eAAe,EAAE,MAAM,WAAW,CAAC;AAC1D,YAAY,EAAE,cAAc,EAAE,MAAM,WAAW,CAAC;AAEhD,YAAY,EAAE,OAAO,EAAE,SAAS,EAAE,WAAW,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAC;AACtF,OAAO,EAAE,aAAa,EAAE,MAAM,sBAAsB,CAAC;AACrD,OAAO,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAC;AAEnD,OAAO,EAAE,gBAAgB,EAAE,MAAM,kBAAkB,CAAC;AACpD,YAAY,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAEnD,OAAO,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAC;AAE9C,OAAO,EAAE,YAAY,EAAE,kBAAkB,EAAE,MAAM,oBAAoB,CAAC;AAEtE,OAAO,EACL,gBAAgB,EAChB,aAAa,EACb,uBAAuB,EACvB,aAAa,GACd,MAAM,wBAAwB,CAAC;AAChC,YAAY,EAAE,cAAc,EAAE,iBAAiB,EAAE,MAAM,wBAAwB,CAAC;AAEhF,OAAO,EAAE,SAAS,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
// @automatalabs/acp-agents — implements the AgentRunner seam from @automatalabs/shared-types over
|
|
2
|
+
// the Agent Client Protocol. It spawns claude-agent-acp / the installed npm dep codex-acp
|
|
3
|
+
// (patched via pnpm patchedDependencies) as child processes and drives one subagent run to
|
|
4
|
+
// completion. It NEVER imports
|
|
5
|
+
// @automatalabs/workflow-engine; the two siblings meet ONLY at AgentRunner, injected by
|
|
6
|
+
// mcp-server (the composition root) via createAcpRunner().
|
|
7
|
+
export { AcpAgentRunner, createAcpRunner, selectBackend } from "./runner.js";
|
|
8
|
+
export { PooledConnection, SessionHandle } from "./acp-client.js";
|
|
9
|
+
export { AcpAgentPool, resolvePoolSize } from "./pool.js";
|
|
10
|
+
export { ClaudeBackend } from "./backends/claude.js";
|
|
11
|
+
export { CodexBackend } from "./backends/codex.js";
|
|
12
|
+
export { decidePermission } from "./permissions.js";
|
|
13
|
+
export { UsageAccumulator } from "./usage.js";
|
|
14
|
+
export { toJsonSchema, toStrictJsonSchema } from "./schema-strict.js";
|
|
15
|
+
export { extractValidated, findJsonBlock, resolveStructuredOutput, validateValue, } from "./structured-output.js";
|
|
16
|
+
export { errorText, mapThrownError } from "./errors-map.js";
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { RequestPermissionRequest, RequestPermissionResponse } from "@agentclientprotocol/sdk";
|
|
2
|
+
export interface ToolPolicy {
|
|
3
|
+
/** Allow-list (agentType `tools`). When non-empty, a tool that matches NOTHING is denied. */
|
|
4
|
+
allow?: string[];
|
|
5
|
+
/** Deny-list (agentType `disallowedTools`), applied after the allow-list. */
|
|
6
|
+
deny?: string[];
|
|
7
|
+
}
|
|
8
|
+
/** Decide the auto-response for one permission request given the tool policy. */
|
|
9
|
+
export declare function decidePermission(request: RequestPermissionRequest, policy: ToolPolicy): RequestPermissionResponse;
|
|
10
|
+
//# sourceMappingURL=permissions.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"permissions.d.ts","sourceRoot":"","sources":["../src/permissions.ts"],"names":[],"mappings":"AAwBA,OAAO,KAAK,EAGV,wBAAwB,EACxB,yBAAyB,EAC1B,MAAM,0BAA0B,CAAC;AAElC,MAAM,WAAW,UAAU;IACzB,6FAA6F;IAC7F,KAAK,CAAC,EAAE,MAAM,EAAE,CAAC;IACjB,6EAA6E;IAC7E,IAAI,CAAC,EAAE,MAAM,EAAE,CAAC;CACjB;AAKD,iFAAiF;AACjF,wBAAgB,gBAAgB,CAC9B,OAAO,EAAE,wBAAwB,EACjC,MAAM,EAAE,UAAU,GACjB,yBAAyB,CAqC3B"}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
const ALLOW_KIND_ORDER = ["allow_once", "allow_always"];
|
|
2
|
+
const REJECT_KIND_ORDER = ["reject_once", "reject_always"];
|
|
3
|
+
/** Decide the auto-response for one permission request given the tool policy. */
|
|
4
|
+
export function decidePermission(request, policy) {
|
|
5
|
+
const { toolNames, decoration } = candidateNames(request);
|
|
6
|
+
// (a) EXACT pool: the authoritative tool id (vendor _meta.toolName) when present, else
|
|
7
|
+
// the human title/kind. The substring pool is everything, used only as a fallback.
|
|
8
|
+
const exactPool = toolNames.length > 0 ? toolNames : decoration;
|
|
9
|
+
const allPool = [...toolNames, ...decoration];
|
|
10
|
+
const denyList = policy.deny ?? [];
|
|
11
|
+
const allowList = policy.allow ?? [];
|
|
12
|
+
const hasDeny = denyList.length > 0;
|
|
13
|
+
const hasAllowList = allowList.length > 0;
|
|
14
|
+
const denyExact = hasDeny && exactMatchesAny(exactPool, denyList);
|
|
15
|
+
const allowExact = hasAllowList && exactMatchesAny(exactPool, allowList);
|
|
16
|
+
let denied;
|
|
17
|
+
let allowedByList;
|
|
18
|
+
if (denyExact || allowExact) {
|
|
19
|
+
// The tool is EXACTLY named by some policy entry -> decide on exact matches alone.
|
|
20
|
+
// (Suppresses loose substring matches: a deny `read` no longer catches an exactly
|
|
21
|
+
// allow-listed `thread-reader`.)
|
|
22
|
+
denied = denyExact;
|
|
23
|
+
allowedByList = !hasAllowList || allowExact;
|
|
24
|
+
}
|
|
25
|
+
else {
|
|
26
|
+
// (b) No exact match in either list -> best-effort bidirectional substring fallback.
|
|
27
|
+
denied = hasDeny && substringMatchesAny(allPool, denyList);
|
|
28
|
+
allowedByList = !hasAllowList || substringMatchesAny(allPool, allowList);
|
|
29
|
+
}
|
|
30
|
+
const wantAllow = !denied && allowedByList;
|
|
31
|
+
const option = pickOption(request.options, wantAllow);
|
|
32
|
+
if (!option) {
|
|
33
|
+
// No option of the desired polarity exists. Cancelling the permission is the only
|
|
34
|
+
// remaining way to refuse a tool the server offers no reject option for.
|
|
35
|
+
return { outcome: { outcome: "cancelled" } };
|
|
36
|
+
}
|
|
37
|
+
return { outcome: { outcome: "selected", optionId: option.optionId } };
|
|
38
|
+
}
|
|
39
|
+
function candidateNames(request) {
|
|
40
|
+
const decoration = [];
|
|
41
|
+
const toolCall = request.toolCall;
|
|
42
|
+
if (toolCall.title)
|
|
43
|
+
decoration.push(toolCall.title);
|
|
44
|
+
if (toolCall.kind)
|
|
45
|
+
decoration.push(toolCall.kind);
|
|
46
|
+
const toolNames = [];
|
|
47
|
+
collectMetaToolNames(toolCall._meta, toolNames);
|
|
48
|
+
return { toolNames, decoration };
|
|
49
|
+
}
|
|
50
|
+
function collectMetaToolNames(meta, out) {
|
|
51
|
+
if (!meta || typeof meta !== "object")
|
|
52
|
+
return;
|
|
53
|
+
for (const [key, value] of Object.entries(meta)) {
|
|
54
|
+
if (key === "toolName" && typeof value === "string")
|
|
55
|
+
out.push(value);
|
|
56
|
+
else if (value && typeof value === "object")
|
|
57
|
+
collectMetaToolNames(value, out);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
/** (a) Does any pattern EXACTLY equal (case-insensitive) any candidate name? */
|
|
61
|
+
function exactMatchesAny(names, patterns) {
|
|
62
|
+
const lowered = new Set(names.map((n) => n.toLowerCase()).filter(Boolean));
|
|
63
|
+
return patterns.some((pattern) => {
|
|
64
|
+
const p = pattern.toLowerCase();
|
|
65
|
+
return p.length > 0 && lowered.has(p);
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
/** (b) Best-effort bidirectional substring fallback (the prior, looser semantics). */
|
|
69
|
+
function substringMatchesAny(names, patterns) {
|
|
70
|
+
const lowered = names.map((n) => n.toLowerCase()).filter(Boolean);
|
|
71
|
+
return patterns.some((pattern) => {
|
|
72
|
+
const p = pattern.toLowerCase();
|
|
73
|
+
if (!p)
|
|
74
|
+
return false;
|
|
75
|
+
return lowered.some((n) => n === p || n.includes(p) || p.includes(n));
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
function pickOption(options, wantAllow) {
|
|
79
|
+
const order = wantAllow ? ALLOW_KIND_ORDER : REJECT_KIND_ORDER;
|
|
80
|
+
for (const kind of order) {
|
|
81
|
+
const found = options.find((option) => option.kind === kind);
|
|
82
|
+
if (found)
|
|
83
|
+
return found;
|
|
84
|
+
}
|
|
85
|
+
return undefined;
|
|
86
|
+
}
|
package/dist/pool.d.ts
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type { Backend } from "./backend.js";
|
|
2
|
+
import { SessionHandle, type AcpSessionOptions } from "./acp-client.js";
|
|
3
|
+
export interface AcpPoolOptions {
|
|
4
|
+
/** Long-lived processes to keep PER backend. Default 1; falls back to AGENTPRISM_ACP_POOL_SIZE. */
|
|
5
|
+
size?: number;
|
|
6
|
+
}
|
|
7
|
+
/** Resolve the per-backend pool size: explicit option wins, else env, else 1. Clamped to >= 1. */
|
|
8
|
+
export declare function resolvePoolSize(option?: number): number;
|
|
9
|
+
export declare class AcpAgentPool {
|
|
10
|
+
private readonly size;
|
|
11
|
+
private readonly byBackend;
|
|
12
|
+
private readonly onProcessExit;
|
|
13
|
+
private exitHookInstalled;
|
|
14
|
+
private disposed;
|
|
15
|
+
constructor(options?: AcpPoolOptions);
|
|
16
|
+
/** Acquire a session for one agent() run: get/grow a pooled connection and open a session. */
|
|
17
|
+
acquire(backend: Backend, opts: AcpSessionOptions): Promise<SessionHandle>;
|
|
18
|
+
/**
|
|
19
|
+
* Pick the connection to host the next session. Runs SYNCHRONOUSLY (no await) through to the
|
|
20
|
+
* synchronous load-reservation in openSession(), so concurrent acquires never over-spawn or
|
|
21
|
+
* double-book a connection.
|
|
22
|
+
*/
|
|
23
|
+
private selectConnection;
|
|
24
|
+
private connectionsFor;
|
|
25
|
+
/** Evict a dead connection so it is never handed out again. */
|
|
26
|
+
private drop;
|
|
27
|
+
/** Close every pooled process and clear the pool. Idempotent. */
|
|
28
|
+
dispose(): Promise<void>;
|
|
29
|
+
private allConnections;
|
|
30
|
+
private installExitHook;
|
|
31
|
+
private removeExitHook;
|
|
32
|
+
/** Synchronous best-effort child kill for the process-exit hook (no async work is possible). */
|
|
33
|
+
private killAllSync;
|
|
34
|
+
}
|
|
35
|
+
//# sourceMappingURL=pool.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"pool.d.ts","sourceRoot":"","sources":["../src/pool.ts"],"names":[],"mappings":"AAYA,OAAO,KAAK,EAAE,OAAO,EAAa,MAAM,cAAc,CAAC;AACvD,OAAO,EAAoB,aAAa,EAAE,KAAK,iBAAiB,EAAE,MAAM,iBAAiB,CAAC;AAK1F,MAAM,WAAW,cAAc;IAC7B,mGAAmG;IACnG,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAED,kGAAkG;AAClG,wBAAgB,eAAe,CAAC,MAAM,CAAC,EAAE,MAAM,GAAG,MAAM,CAUvD;AAED,qBAAa,YAAY;IACvB,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAS;IAC9B,OAAO,CAAC,QAAQ,CAAC,SAAS,CAA4C;IACtE,OAAO,CAAC,QAAQ,CAAC,aAAa,CAA4B;IAC1D,OAAO,CAAC,iBAAiB,CAAS;IAClC,OAAO,CAAC,QAAQ,CAAS;gBAEb,OAAO,GAAE,cAAmB;IAIxC,8FAA8F;IACxF,OAAO,CAAC,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,iBAAiB,GAAG,OAAO,CAAC,aAAa,CAAC;IAMhF;;;;OAIG;IACH,OAAO,CAAC,gBAAgB;IAoBxB,OAAO,CAAC,cAAc;IAStB,+DAA+D;IAC/D,OAAO,CAAC,IAAI;IAOZ,iEAAiE;IAC3D,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC;IAQ9B,OAAO,CAAC,cAAc;IAMtB,OAAO,CAAC,eAAe;IAMvB,OAAO,CAAC,cAAc;IAMtB,gGAAgG;IAChG,OAAO,CAAC,WAAW;CAGpB"}
|
package/dist/pool.js
ADDED
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { PooledConnection } from "./acp-client.js";
|
|
2
|
+
const DEFAULT_POOL_SIZE = 1;
|
|
3
|
+
const POOL_SIZE_ENV = "AGENTPRISM_ACP_POOL_SIZE";
|
|
4
|
+
/** Resolve the per-backend pool size: explicit option wins, else env, else 1. Clamped to >= 1. */
|
|
5
|
+
export function resolvePoolSize(option) {
|
|
6
|
+
if (typeof option === "number" && Number.isFinite(option) && option >= 1) {
|
|
7
|
+
return Math.floor(option);
|
|
8
|
+
}
|
|
9
|
+
const env = process.env[POOL_SIZE_ENV];
|
|
10
|
+
if (env !== undefined) {
|
|
11
|
+
const parsed = Number.parseInt(env, 10);
|
|
12
|
+
if (Number.isFinite(parsed) && parsed >= 1)
|
|
13
|
+
return parsed;
|
|
14
|
+
}
|
|
15
|
+
return DEFAULT_POOL_SIZE;
|
|
16
|
+
}
|
|
17
|
+
export class AcpAgentPool {
|
|
18
|
+
size;
|
|
19
|
+
byBackend = new Map();
|
|
20
|
+
onProcessExit = () => this.killAllSync();
|
|
21
|
+
exitHookInstalled = false;
|
|
22
|
+
disposed = false;
|
|
23
|
+
constructor(options = {}) {
|
|
24
|
+
this.size = resolvePoolSize(options.size);
|
|
25
|
+
}
|
|
26
|
+
/** Acquire a session for one agent() run: get/grow a pooled connection and open a session. */
|
|
27
|
+
async acquire(backend, opts) {
|
|
28
|
+
if (this.disposed)
|
|
29
|
+
throw new Error("ACP agent pool is disposed");
|
|
30
|
+
const connection = this.selectConnection(backend);
|
|
31
|
+
return connection.openSession(opts);
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Pick the connection to host the next session. Runs SYNCHRONOUSLY (no await) through to the
|
|
35
|
+
* synchronous load-reservation in openSession(), so concurrent acquires never over-spawn or
|
|
36
|
+
* double-book a connection.
|
|
37
|
+
*/
|
|
38
|
+
selectConnection(backend) {
|
|
39
|
+
const connections = this.connectionsFor(backend.id);
|
|
40
|
+
const live = connections.filter((c) => c.alive);
|
|
41
|
+
const idle = live.find((c) => c.activeSessions === 0);
|
|
42
|
+
if (idle)
|
|
43
|
+
return idle;
|
|
44
|
+
if (live.length < this.size) {
|
|
45
|
+
this.installExitHook();
|
|
46
|
+
const connection = PooledConnection.create(backend, {
|
|
47
|
+
onDead: (dead) => this.drop(backend.id, dead),
|
|
48
|
+
});
|
|
49
|
+
connections.push(connection);
|
|
50
|
+
return connection;
|
|
51
|
+
}
|
|
52
|
+
// At capacity with every connection busy: multiplex onto the least-loaded one.
|
|
53
|
+
return live.reduce((least, c) => (c.activeSessions < least.activeSessions ? c : least));
|
|
54
|
+
}
|
|
55
|
+
connectionsFor(id) {
|
|
56
|
+
let arr = this.byBackend.get(id);
|
|
57
|
+
if (!arr) {
|
|
58
|
+
arr = [];
|
|
59
|
+
this.byBackend.set(id, arr);
|
|
60
|
+
}
|
|
61
|
+
return arr;
|
|
62
|
+
}
|
|
63
|
+
/** Evict a dead connection so it is never handed out again. */
|
|
64
|
+
drop(id, connection) {
|
|
65
|
+
const arr = this.byBackend.get(id);
|
|
66
|
+
if (!arr)
|
|
67
|
+
return;
|
|
68
|
+
const index = arr.indexOf(connection);
|
|
69
|
+
if (index >= 0)
|
|
70
|
+
arr.splice(index, 1);
|
|
71
|
+
}
|
|
72
|
+
/** Close every pooled process and clear the pool. Idempotent. */
|
|
73
|
+
async dispose() {
|
|
74
|
+
this.disposed = true;
|
|
75
|
+
this.removeExitHook();
|
|
76
|
+
const all = this.allConnections();
|
|
77
|
+
this.byBackend.clear();
|
|
78
|
+
await Promise.all(all.map((c) => c.dispose()));
|
|
79
|
+
}
|
|
80
|
+
allConnections() {
|
|
81
|
+
const all = [];
|
|
82
|
+
for (const arr of this.byBackend.values())
|
|
83
|
+
all.push(...arr);
|
|
84
|
+
return all;
|
|
85
|
+
}
|
|
86
|
+
installExitHook() {
|
|
87
|
+
if (this.exitHookInstalled)
|
|
88
|
+
return;
|
|
89
|
+
this.exitHookInstalled = true;
|
|
90
|
+
process.once("exit", this.onProcessExit);
|
|
91
|
+
}
|
|
92
|
+
removeExitHook() {
|
|
93
|
+
if (!this.exitHookInstalled)
|
|
94
|
+
return;
|
|
95
|
+
this.exitHookInstalled = false;
|
|
96
|
+
process.removeListener("exit", this.onProcessExit);
|
|
97
|
+
}
|
|
98
|
+
/** Synchronous best-effort child kill for the process-exit hook (no async work is possible). */
|
|
99
|
+
killAllSync() {
|
|
100
|
+
for (const connection of this.allConnections())
|
|
101
|
+
connection.killNow();
|
|
102
|
+
}
|
|
103
|
+
}
|
package/dist/runner.d.ts
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { type AgentResult, type AgentRunner, type RunOptions } from "@automatalabs/shared-types";
|
|
2
|
+
import type { TSchema } from "typebox";
|
|
3
|
+
import { type AcpPoolOptions } from "./pool.js";
|
|
4
|
+
import type { Backend } from "./backend.js";
|
|
5
|
+
export declare class AcpAgentRunner implements AgentRunner {
|
|
6
|
+
private readonly pool;
|
|
7
|
+
constructor(options?: AcpPoolOptions);
|
|
8
|
+
run<S extends TSchema | undefined = undefined>(prompt: string, options?: RunOptions<S>): Promise<AgentResult<S>>;
|
|
9
|
+
/** Tear down the whole pool (close every long-lived process). Call when the run ends / the
|
|
10
|
+
* runner is disposed. Beyond the AgentRunner seam (additive) — never enters the resume hash. */
|
|
11
|
+
dispose(): Promise<void>;
|
|
12
|
+
}
|
|
13
|
+
/** Factory the mcp-server composition root calls to inject the runner into the engine. The pool
|
|
14
|
+
* size is a runner-level option (default 1, else AGENTPRISM_ACP_POOL_SIZE) — NOT a RunOptions
|
|
15
|
+
* field, so it never enters hashAgentCall / the resume identity. */
|
|
16
|
+
export declare function createAcpRunner(options?: AcpPoolOptions): AcpAgentRunner;
|
|
17
|
+
/** Pick the backend by model/tier. Cross-provider routing = which ACP server to spawn. */
|
|
18
|
+
export declare function selectBackend(opts: {
|
|
19
|
+
model?: string;
|
|
20
|
+
tier?: string;
|
|
21
|
+
}): Backend;
|
|
22
|
+
//# sourceMappingURL=runner.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"runner.d.ts","sourceRoot":"","sources":["../src/runner.ts"],"names":[],"mappings":"AAkBA,OAAO,EAGL,KAAK,WAAW,EAChB,KAAK,WAAW,EAChB,KAAK,UAAU,EAChB,MAAM,4BAA4B,CAAC;AAEpC,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,SAAS,CAAC;AAEvC,OAAO,EAAgB,KAAK,cAAc,EAAE,MAAM,WAAW,CAAC;AAC9D,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,cAAc,CAAC;AAS5C,qBAAa,cAAe,YAAW,WAAW;IAChD,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAe;gBAExB,OAAO,GAAE,cAAmB;IAIlC,GAAG,CAAC,CAAC,SAAS,OAAO,GAAG,SAAS,GAAG,SAAS,EACjD,MAAM,EAAE,MAAM,EACd,OAAO,GAAE,UAAU,CAAC,CAAC,CAAM,GAC1B,OAAO,CAAC,WAAW,CAAC,CAAC,CAAC,CAAC;IAiF1B;qGACiG;IAC3F,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC;CAG/B;AAED;;qEAEqE;AACrE,wBAAgB,eAAe,CAAC,OAAO,CAAC,EAAE,cAAc,GAAG,cAAc,CAExE;AAoED,0FAA0F;AAC1F,wBAAgB,aAAa,CAAC,IAAI,EAAE;IAAE,KAAK,CAAC,EAAE,MAAM,CAAC;IAAC,IAAI,CAAC,EAAE,MAAM,CAAA;CAAE,GAAG,OAAO,CAG9E"}
|