@osdlc/agent-call 0.0.1-stage → 0.4.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.
@@ -0,0 +1,40 @@
1
+ import type { CapabilityNote, Support } from '../plan.js';
2
+ import type { AgentCallSpec, PermissionMode } from '../spec.js';
3
+ /** `--model <model>`: the flag claude, codex, antigravity and pi all take. */
4
+ export declare function modelFlag(model: string): string[];
5
+ export interface PermissionModeEntry {
6
+ argv: string[];
7
+ support: Extract<Support, 'native' | 'approximated'>;
8
+ detail: string;
9
+ }
10
+ export type PermissionModeTable = Partial<Record<Exclude<PermissionMode, 'default'>, PermissionModeEntry>>;
11
+ /** permissions.mode → argv tokens, shared by adapters that express modes as CLI flags. */
12
+ export declare function applyPermissionMode(argv: string[], notes: CapabilityNote[], mode: PermissionMode | undefined, table: PermissionModeTable): void;
13
+ /** Which spec fields the target CLI handles natively (everything else gets emulated in the prompt). */
14
+ export interface PromptEmulation {
15
+ /** CLI attaches context files natively; skip the file preamble. */
16
+ filesNative?: boolean | undefined;
17
+ /** Prefix file entries with `@` for agents with @-inclusion syntax. */
18
+ filesAtSyntax?: boolean | undefined;
19
+ /** CLI enforces an output schema natively; skip the JSON instruction. */
20
+ schemaNative?: boolean | undefined;
21
+ /** CLI takes system prompts natively; skip the system preamble. */
22
+ systemNative?: boolean;
23
+ /** CLI takes appended system prompts natively; skip the instructions preamble. */
24
+ appendSystemNative?: boolean;
25
+ }
26
+ /**
27
+ * Build the prompt actually sent, folding in every spec field the target CLI
28
+ * cannot express natively. Each fold is recorded as an `emulated` note.
29
+ */
30
+ export declare function buildEffectivePrompt(spec: AgentCallSpec, emulation: PromptEmulation, notes: CapabilityNote[]): string;
31
+ /** Strip one surrounding markdown code fence, if present. */
32
+ export declare function stripCodeFence(text: string): string;
33
+ /** Parse JSON, tolerating a surrounding code fence. Returns undefined on failure. */
34
+ export declare function tryParseJson(text: string): unknown;
35
+ /** Parse line-delimited JSON, skipping lines that aren't JSON objects. */
36
+ export declare function parseJsonLines(stdout: string): unknown[];
37
+ /** Type-tolerant object field access for parsing loosely specified payloads. */
38
+ export declare function get(value: unknown, ...path: string[]): unknown;
39
+ export declare function asString(value: unknown): string | undefined;
40
+ export declare function asNumber(value: unknown): number | undefined;
@@ -0,0 +1,101 @@
1
+ import { note } from '../plan.js';
2
+ /** `--model <model>`: the flag claude, codex, antigravity and pi all take. */
3
+ export function modelFlag(model) {
4
+ return ['--model', model];
5
+ }
6
+ /** permissions.mode → argv tokens, shared by adapters that express modes as CLI flags. */
7
+ export function applyPermissionMode(argv, notes, mode, table) {
8
+ if (mode === undefined || mode === 'default')
9
+ return;
10
+ const entry = table[mode];
11
+ if (entry === undefined)
12
+ return;
13
+ argv.push(...entry.argv);
14
+ note(notes, 'permissions.mode', entry.support, entry.detail);
15
+ }
16
+ /**
17
+ * Build the prompt actually sent, folding in every spec field the target CLI
18
+ * cannot express natively. Each fold is recorded as an `emulated` note.
19
+ */
20
+ export function buildEffectivePrompt(spec, emulation, notes) {
21
+ const sections = [];
22
+ if (spec.systemPrompt !== undefined && !emulation.systemNative) {
23
+ sections.push(`<system>\n${spec.systemPrompt}\n</system>`);
24
+ note(notes, 'systemPrompt', 'emulated', 'no system-prompt flag; prepended to the prompt in a <system> block');
25
+ }
26
+ if (spec.appendSystemPrompt !== undefined && !emulation.appendSystemNative) {
27
+ sections.push(`<instructions>\n${spec.appendSystemPrompt}\n</instructions>`);
28
+ note(notes, 'appendSystemPrompt', 'emulated', 'no append-system-prompt flag; prepended to the prompt in an <instructions> block');
29
+ }
30
+ if (spec.context?.text !== undefined) {
31
+ sections.push(`<context>\n${spec.context.text}\n</context>`);
32
+ }
33
+ const files = spec.context?.files ?? [];
34
+ if (files.length > 0 && !emulation.filesNative) {
35
+ const listed = files.map((f) => (emulation.filesAtSyntax ? `@${f}` : `- ${f}`)).join('\n');
36
+ sections.push(`Relevant files (read them before answering):\n${listed}`);
37
+ note(notes, 'context.files', 'emulated', emulation.filesAtSyntax
38
+ ? 'no attach flag; referenced with @path syntax in the prompt'
39
+ : 'no attach flag; listed in a prompt preamble for the agent to read');
40
+ }
41
+ sections.push(spec.prompt);
42
+ if (spec.output?.schema !== undefined && !emulation.schemaNative) {
43
+ sections.push('Respond with ONLY a JSON value (no prose, no markdown fences) that ' +
44
+ `validates against this JSON Schema:\n${JSON.stringify(spec.output.schema)}`);
45
+ note(notes, 'output.schema', 'emulated', 'no schema flag; appended a respond-only-with-JSON instruction to the prompt');
46
+ }
47
+ return sections.join('\n\n');
48
+ }
49
+ /** Strip one surrounding markdown code fence, if present. */
50
+ export function stripCodeFence(text) {
51
+ const trimmed = text.trim();
52
+ if (!trimmed.startsWith('```'))
53
+ return trimmed;
54
+ const lines = trimmed.split('\n');
55
+ if (lines[0]?.startsWith('```'))
56
+ lines.shift();
57
+ if (lines[lines.length - 1]?.trim().startsWith('```'))
58
+ lines.pop();
59
+ return lines.join('\n').trim();
60
+ }
61
+ /** Parse JSON, tolerating a surrounding code fence. Returns undefined on failure. */
62
+ export function tryParseJson(text) {
63
+ try {
64
+ return JSON.parse(stripCodeFence(text));
65
+ }
66
+ catch {
67
+ return undefined;
68
+ }
69
+ }
70
+ /** Parse line-delimited JSON, skipping lines that aren't JSON objects. */
71
+ export function parseJsonLines(stdout) {
72
+ const events = [];
73
+ for (const line of stdout.split('\n')) {
74
+ const trimmed = line.trim();
75
+ if (!trimmed.startsWith('{'))
76
+ continue;
77
+ try {
78
+ events.push(JSON.parse(trimmed));
79
+ }
80
+ catch {
81
+ // Interleaved log noise is expected on some CLIs; skip it.
82
+ }
83
+ }
84
+ return events;
85
+ }
86
+ /** Type-tolerant object field access for parsing loosely specified payloads. */
87
+ export function get(value, ...path) {
88
+ let current = value;
89
+ for (const key of path) {
90
+ if (current === null || typeof current !== 'object')
91
+ return undefined;
92
+ current = current[key];
93
+ }
94
+ return current;
95
+ }
96
+ export function asString(value) {
97
+ return typeof value === 'string' ? value : undefined;
98
+ }
99
+ export function asNumber(value) {
100
+ return typeof value === 'number' && Number.isFinite(value) ? value : undefined;
101
+ }
@@ -0,0 +1,43 @@
1
+ import type { ModelChoice } from '../model-args.js';
2
+ import type { AgentAdapter } from './types.js';
3
+ /**
4
+ * Options for {@link templateAdapter}.
5
+ *
6
+ * @experimental Part of the launch-argv helpers, which may change in a minor release.
7
+ */
8
+ export interface TemplateAdapterOptions {
9
+ /** Adapter name, reported as `PlannedCall.agent`. */
10
+ name: string;
11
+ /** Binary to spawn. */
12
+ binary: string;
13
+ /**
14
+ * Arguments after the binary. `{prompt}`, `{model}` and `{effort}` are
15
+ * slots: an argument equal to one is replaced whole by its value; any other
16
+ * argument passes as written, so a value never lands inside a larger one.
17
+ */
18
+ args: readonly string[];
19
+ /** Whether the prompt fills the `{prompt}` slot or goes on stdin. */
20
+ promptVia: 'argv' | 'stdin';
21
+ }
22
+ /**
23
+ * Why a template with `args` cannot take `choice`, or `null` when it can: a
24
+ * model or effort needs its slot, and a slot needs a value.
25
+ *
26
+ * @experimental Part of the launch-argv helpers, which may change in a minor release.
27
+ */
28
+ export declare function templateRefusal(args: readonly string[], choice: ModelChoice): string | null;
29
+ /**
30
+ * A headless call through an argument template for any one-shot CLI: the slots
31
+ * are filled and the prompt goes in `{prompt}` or on stdin. Context, system
32
+ * prompt and schema are folded into the prompt; `permissions.mode` is noted
33
+ * as dropped. Any other field the template cannot express (budget, session,
34
+ * mcp, plugins, dirs, tool rules) is left out with no note, as
35
+ * {@link customAdapter} does.
36
+ *
37
+ * @throws {Error} From `plan` (so from `planCall` and `callAgent`) when the
38
+ * spec's model or effort does not fit the slots ({@link templateRefusal}).
39
+ * @throws {Error} From `templateAdapter` itself when `promptVia` is `'argv'` and
40
+ * `args` has no `{prompt}` slot, or when it is `'stdin'` and `args` has one.
41
+ * @experimental Part of the launch-argv helpers, which may change in a minor release.
42
+ */
43
+ export declare function templateAdapter(options: TemplateAdapterOptions): AgentAdapter;
@@ -0,0 +1,62 @@
1
+ import { customAdapter } from './custom.js';
2
+ /**
3
+ * Why a template with `args` cannot take `choice`, or `null` when it can: a
4
+ * model or effort needs its slot, and a slot needs a value.
5
+ *
6
+ * @experimental Part of the launch-argv helpers, which may change in a minor release.
7
+ */
8
+ export function templateRefusal(args, choice) {
9
+ for (const [field, slot] of [
10
+ ['model', '{model}'],
11
+ ['effort', '{effort}'],
12
+ ]) {
13
+ const value = choice[field];
14
+ const has = args.includes(slot);
15
+ if (value !== undefined && !has) {
16
+ return `its headless template has no ${slot} slot; cannot pass ${field} '${value}'`;
17
+ }
18
+ if (value === undefined && has) {
19
+ return `its headless template's ${slot} slot needs a ${field}, and none was resolved`;
20
+ }
21
+ }
22
+ return null;
23
+ }
24
+ /**
25
+ * A headless call through an argument template for any one-shot CLI: the slots
26
+ * are filled and the prompt goes in `{prompt}` or on stdin. Context, system
27
+ * prompt and schema are folded into the prompt; `permissions.mode` is noted
28
+ * as dropped. Any other field the template cannot express (budget, session,
29
+ * mcp, plugins, dirs, tool rules) is left out with no note, as
30
+ * {@link customAdapter} does.
31
+ *
32
+ * @throws {Error} From `plan` (so from `planCall` and `callAgent`) when the
33
+ * spec's model or effort does not fit the slots ({@link templateRefusal}).
34
+ * @throws {Error} From `templateAdapter` itself when `promptVia` is `'argv'` and
35
+ * `args` has no `{prompt}` slot, or when it is `'stdin'` and `args` has one.
36
+ * @experimental Part of the launch-argv helpers, which may change in a minor release.
37
+ */
38
+ export function templateAdapter(options) {
39
+ const hasPromptSlot = options.args.includes('{prompt}');
40
+ if (options.promptVia === 'argv' && !hasPromptSlot) {
41
+ throw new Error(`${options.name}: promptVia 'argv' needs a {prompt} slot in args`);
42
+ }
43
+ if (options.promptVia === 'stdin' && hasPromptSlot) {
44
+ throw new Error(`${options.name}: promptVia 'stdin' cannot have a {prompt} slot in args`);
45
+ }
46
+ return customAdapter({
47
+ name: options.name,
48
+ binary: options.binary,
49
+ promptVia: options.promptVia,
50
+ args: (spec, prompt) => {
51
+ const refusal = templateRefusal(options.args, spec);
52
+ if (refusal !== null)
53
+ throw new Error(`${options.name}: ${refusal}`);
54
+ const slots = {
55
+ '{prompt}': prompt,
56
+ '{model}': spec.model,
57
+ '{effort}': spec.effort,
58
+ };
59
+ return options.args.map((arg) => (Object.hasOwn(slots, arg) ? slots[arg] : arg));
60
+ },
61
+ });
62
+ }
@@ -0,0 +1,55 @@
1
+ import type { Conversation, Usage } from '../conversation.js';
2
+ import type { PlannedCall } from '../plan.js';
3
+ import type { AgentCallSpec } from '../spec.js';
4
+ /** What actually came back from the child process. */
5
+ export interface RawProcessOutput {
6
+ stdout: string;
7
+ stderr: string;
8
+ exitCode: number | null;
9
+ durationMs: number;
10
+ /** The run was cut short: a timeout elapsed, or the caller aborted. */
11
+ timedOut: boolean;
12
+ /** Which came first when the run was cut short: the timeout or a caller abort. */
13
+ cutShortBy?: 'timeout' | 'abort' | undefined;
14
+ /** The signal that ended the child, when it died on one (for example `SIGKILL`). */
15
+ signal?: string | undefined;
16
+ /**
17
+ * The prompt could not be fully written to the child's stdin, because the
18
+ * child exited or closed the pipe first — so the agent answered a truncated
19
+ * prompt. Absent when the write completed.
20
+ */
21
+ stdinError?: string | undefined;
22
+ /** Contents of `readBack` temp files, keyed by `TempFile.name`. */
23
+ files?: Record<string, string> | undefined;
24
+ }
25
+ /** Adapter-parsed view of a raw run, before assembly into an AgentResult. */
26
+ export interface ParsedOutput {
27
+ /** Best-effort final assistant answer as plain text. */
28
+ text: string;
29
+ /** Parsed JSON answer when `output.schema` was requested. */
30
+ structured?: unknown | undefined;
31
+ sessionId?: string | undefined;
32
+ usage?: Usage | undefined;
33
+ costUsd?: number | undefined;
34
+ model?: string | undefined;
35
+ stopReason?: string | undefined;
36
+ /** Parsed JSONL events when a streaming format was used. */
37
+ events?: unknown[] | undefined;
38
+ /** The agent itself reported failure (distinct from a nonzero exit). */
39
+ isError?: boolean | undefined;
40
+ errorDetail?: string | undefined;
41
+ }
42
+ /**
43
+ * One agent CLI. `plan` is pure (spec in, invocation out); `parseResult`
44
+ * is pure (bytes in, structure out). All process handling lives in the
45
+ * runner, so adapters are trivially unit-testable.
46
+ */
47
+ export interface AgentAdapter {
48
+ name: string;
49
+ /** Binary to spawn; override via each adapter factory's `binary` option. */
50
+ binary: string;
51
+ plan(spec: AgentCallSpec): PlannedCall;
52
+ parseResult(planned: PlannedCall, raw: RawProcessOutput): ParsedOutput;
53
+ /** Optional supplemental layer: lift the raw transcript into the normalized model. */
54
+ toConversation?(planned: PlannedCall, raw: RawProcessOutput): Conversation | undefined;
55
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,73 @@
1
+ /**
2
+ * The normalized conversation data model — the optional layer on top of the
3
+ * caller abstraction. Every adapter can lift its raw transcript into this
4
+ * shape for post-facto analysis; the raw payload is always preserved in
5
+ * `Conversation.raw` so normalization never loses information.
6
+ *
7
+ * The shape follows the common core of the OpenTelemetry GenAI conventions
8
+ * and the Vercel AI SDK message model: role-tagged messages made of typed
9
+ * parts (text, reasoning, tool-call, tool-result), plus run-level usage,
10
+ * cost, and timing.
11
+ */
12
+ /** The `Conversation.schema` value of this data model; its version is part of the string. */
13
+ export declare const CONVERSATION_SCHEMA: "agent-conversation/1";
14
+ /** Who produced a message. Tool results travel under `tool`. */
15
+ export type Role = 'system' | 'user' | 'assistant' | 'tool';
16
+ /** One typed piece of a message: text, reasoning, a tool call, or a tool result. */
17
+ export type Part = {
18
+ type: 'text';
19
+ text: string;
20
+ } | {
21
+ type: 'reasoning';
22
+ text: string;
23
+ } | {
24
+ type: 'tool-call';
25
+ callId?: string | undefined;
26
+ name: string;
27
+ input: unknown;
28
+ } | {
29
+ type: 'tool-result';
30
+ callId?: string | undefined;
31
+ name?: string | undefined;
32
+ output: unknown;
33
+ isError?: boolean | undefined;
34
+ };
35
+ /** One message of a conversation: a role and its typed parts. */
36
+ export interface ConversationMessage {
37
+ role: Role;
38
+ parts: Part[];
39
+ /** ISO 8601, when the source transcript carries timestamps. */
40
+ timestamp?: string | undefined;
41
+ }
42
+ /** Token counts for a run. Each field is present only when the agent reports it. */
43
+ export interface Usage {
44
+ inputTokens?: number | undefined;
45
+ outputTokens?: number | undefined;
46
+ cacheReadTokens?: number | undefined;
47
+ cacheWriteTokens?: number | undefined;
48
+ totalTokens?: number | undefined;
49
+ }
50
+ /** A run lifted into the normalized model, with the untouched source kept in `raw`. */
51
+ export interface Conversation {
52
+ schema: typeof CONVERSATION_SCHEMA;
53
+ /** Adapter name, e.g. "claude". */
54
+ agent: string;
55
+ model?: string | undefined;
56
+ sessionId?: string | undefined;
57
+ messages: ConversationMessage[];
58
+ usage?: Usage | undefined;
59
+ costUsd?: number | undefined;
60
+ durationMs?: number | undefined;
61
+ stopReason?: string | undefined;
62
+ /** The untouched source transcript. `format` names the source encoding. */
63
+ raw: {
64
+ format: string;
65
+ data: unknown;
66
+ };
67
+ }
68
+ /** Concatenated text of every `text` part from assistant messages. */
69
+ export declare function assistantText(conversation: Conversation): string;
70
+ /** Every tool call across the conversation, in order. */
71
+ export declare function toolCalls(conversation: Conversation): Array<Extract<Part, {
72
+ type: 'tool-call';
73
+ }>>;
@@ -0,0 +1,37 @@
1
+ /**
2
+ * The normalized conversation data model — the optional layer on top of the
3
+ * caller abstraction. Every adapter can lift its raw transcript into this
4
+ * shape for post-facto analysis; the raw payload is always preserved in
5
+ * `Conversation.raw` so normalization never loses information.
6
+ *
7
+ * The shape follows the common core of the OpenTelemetry GenAI conventions
8
+ * and the Vercel AI SDK message model: role-tagged messages made of typed
9
+ * parts (text, reasoning, tool-call, tool-result), plus run-level usage,
10
+ * cost, and timing.
11
+ */
12
+ /** The `Conversation.schema` value of this data model; its version is part of the string. */
13
+ export const CONVERSATION_SCHEMA = 'agent-conversation/1';
14
+ /** Concatenated text of every `text` part from assistant messages. */
15
+ export function assistantText(conversation) {
16
+ const chunks = [];
17
+ for (const message of conversation.messages) {
18
+ if (message.role !== 'assistant')
19
+ continue;
20
+ for (const part of message.parts) {
21
+ if (part.type === 'text' && part.text !== '')
22
+ chunks.push(part.text);
23
+ }
24
+ }
25
+ return chunks.join('\n');
26
+ }
27
+ /** Every tool call across the conversation, in order. */
28
+ export function toolCalls(conversation) {
29
+ const calls = [];
30
+ for (const message of conversation.messages) {
31
+ for (const part of message.parts) {
32
+ if (part.type === 'tool-call')
33
+ calls.push(part);
34
+ }
35
+ }
36
+ return calls;
37
+ }
@@ -0,0 +1,26 @@
1
+ export { EFFORT_LEVELS } from './models.js';
2
+ export type { EffortLevel } from './models.js';
3
+ export type { AgentCallSpec, AgentName, ContextSpec, McpSpec, OutputFormat, OutputSpec, PermissionMode, PermissionSpec, PluginSpec, SessionSpec, } from './spec.js';
4
+ export { TMP_TOKEN } from './plan.js';
5
+ export type { CapabilityNote, PlannedCall, Support, TempFile } from './plan.js';
6
+ export { modelArgs, modelArgsRefusal } from './model-args.js';
7
+ export type { ModelChoice } from './model-args.js';
8
+ export { callAgent, planCall } from './run.js';
9
+ export type { AgentResult, CallError, CallOptions } from './run.js';
10
+ export { adapterFor, BUILTIN_AGENTS, detectAgents } from './registry.js';
11
+ export type { AgentAvailability } from './registry.js';
12
+ export { claudeAdapter, claudePluginArgv, claudeResumeFlag } from './adapters/claude.js';
13
+ export type { ClaudeAdapterOptions } from './adapters/claude.js';
14
+ export { codexAdapter } from './adapters/codex.js';
15
+ export type { CodexAdapterOptions } from './adapters/codex.js';
16
+ export { antigravityAdapter } from './adapters/antigravity.js';
17
+ export type { AntigravityAdapterOptions } from './adapters/antigravity.js';
18
+ export { opencodeAdapter } from './adapters/opencode.js';
19
+ export type { OpencodeAdapterOptions } from './adapters/opencode.js';
20
+ export { customAdapter } from './adapters/custom.js';
21
+ export type { CustomAdapterOptions } from './adapters/custom.js';
22
+ export { templateAdapter, templateRefusal } from './adapters/template.js';
23
+ export type { TemplateAdapterOptions } from './adapters/template.js';
24
+ export type { AgentAdapter, ParsedOutput, RawProcessOutput } from './adapters/types.js';
25
+ export { assistantText, CONVERSATION_SCHEMA, toolCalls } from './conversation.js';
26
+ export type { Conversation, ConversationMessage, Part, Role, Usage } from './conversation.js';
package/dist/index.js ADDED
@@ -0,0 +1,12 @@
1
+ export { EFFORT_LEVELS } from './models.js';
2
+ export { TMP_TOKEN } from './plan.js';
3
+ export { modelArgs, modelArgsRefusal } from './model-args.js';
4
+ export { callAgent, planCall } from './run.js';
5
+ export { adapterFor, BUILTIN_AGENTS, detectAgents } from './registry.js';
6
+ export { claudeAdapter, claudePluginArgv, claudeResumeFlag } from './adapters/claude.js';
7
+ export { codexAdapter } from './adapters/codex.js';
8
+ export { antigravityAdapter } from './adapters/antigravity.js';
9
+ export { opencodeAdapter } from './adapters/opencode.js';
10
+ export { customAdapter } from './adapters/custom.js';
11
+ export { templateAdapter, templateRefusal } from './adapters/template.js';
12
+ export { assistantText, CONVERSATION_SCHEMA, toolCalls } from './conversation.js';
@@ -0,0 +1,37 @@
1
+ /**
2
+ * A resolved model and effort to hand a tool's CLI; all optional, none validated here.
3
+ *
4
+ * @experimental Part of the launch-argv helpers, which may change in a minor release.
5
+ */
6
+ export interface ModelChoice {
7
+ model?: string | undefined;
8
+ effort?: string | undefined;
9
+ /**
10
+ * The launch mode the model's provider needs, as flags (codex reaches a
11
+ * local Ollama model only as `--oss --local-provider ollama`). Passed
12
+ * before the model flag.
13
+ */
14
+ via?: readonly string[] | undefined;
15
+ }
16
+ /**
17
+ * Why `tool`'s argv cannot take `choice`, or `null` when it can (a tool with
18
+ * nothing asked of it always can). Only claude, codex and pi have model and
19
+ * effort flags wired here; any other tool refuses rather than dropping the
20
+ * choice silently.
21
+ *
22
+ * @experimental Part of the launch-argv helpers, which may change in a minor release.
23
+ */
24
+ export declare function modelArgsRefusal(tool: string, choice: ModelChoice): string | null;
25
+ /**
26
+ * The CLI flags that select `choice` for `tool`, using the headless adapters'
27
+ * own flag rules: claude `--model <m> --effort <e>`, codex `--model <m> -c
28
+ * model_reasoning_effort="<e>"`, pi `--model <m> --thinking <e>` (the model as
29
+ * given, `provider/id` included, e.g. `ollama/qwen3:4b`). A launch mode
30
+ * (`choice.via`) goes first: codex `--oss --local-provider ollama --model
31
+ * gemma3:4b`. Empty when nothing is set.
32
+ *
33
+ * @throws {Error} For a tool with no flags wired, naming the tool and what was
34
+ * asked ({@link modelArgsRefusal} gives the same text without throwing).
35
+ * @experimental Part of the launch-argv helpers, which may change in a minor release.
36
+ */
37
+ export declare function modelArgs(tool: string, choice: ModelChoice): string[];
@@ -0,0 +1,58 @@
1
+ import { claudeEffortArgv } from './adapters/claude.js';
2
+ import { codexEffortArgv } from './adapters/codex.js';
3
+ import { modelFlag } from './adapters/shared.js';
4
+ /**
5
+ * Each tool's effort flag, keyed by tool name: the tools whose command line a
6
+ * caller builds itself with a model and an effort. A tool run through an
7
+ * adapter or a template translates its own. pi's `--thinking <level>` takes
8
+ * `off` through `max`, so a level passes through as given.
9
+ */
10
+ const EFFORT_ARGV = {
11
+ claude: claudeEffortArgv,
12
+ codex: codexEffortArgv,
13
+ pi: (effort) => ['--thinking', effort],
14
+ };
15
+ /**
16
+ * Why `tool`'s argv cannot take `choice`, or `null` when it can (a tool with
17
+ * nothing asked of it always can). Only claude, codex and pi have model and
18
+ * effort flags wired here; any other tool refuses rather than dropping the
19
+ * choice silently.
20
+ *
21
+ * @experimental Part of the launch-argv helpers, which may change in a minor release.
22
+ */
23
+ export function modelArgsRefusal(tool, choice) {
24
+ const via = choice.via ?? [];
25
+ if (choice.model === undefined && choice.effort === undefined && via.length === 0)
26
+ return null;
27
+ if (Object.hasOwn(EFFORT_ARGV, tool))
28
+ return null;
29
+ const asked = [
30
+ ...(via.length > 0 ? [`launch mode '${via.join(' ')}'`] : []),
31
+ ...(choice.model !== undefined ? [`model '${choice.model}'`] : []),
32
+ ...(choice.effort !== undefined ? [`effort '${choice.effort}'`] : []),
33
+ ].join(' and ');
34
+ return `tool '${tool}' has no model or effort flags wired; cannot pass ${asked}`;
35
+ }
36
+ /**
37
+ * The CLI flags that select `choice` for `tool`, using the headless adapters'
38
+ * own flag rules: claude `--model <m> --effort <e>`, codex `--model <m> -c
39
+ * model_reasoning_effort="<e>"`, pi `--model <m> --thinking <e>` (the model as
40
+ * given, `provider/id` included, e.g. `ollama/qwen3:4b`). A launch mode
41
+ * (`choice.via`) goes first: codex `--oss --local-provider ollama --model
42
+ * gemma3:4b`. Empty when nothing is set.
43
+ *
44
+ * @throws {Error} For a tool with no flags wired, naming the tool and what was
45
+ * asked ({@link modelArgsRefusal} gives the same text without throwing).
46
+ * @experimental Part of the launch-argv helpers, which may change in a minor release.
47
+ */
48
+ export function modelArgs(tool, choice) {
49
+ const refusal = modelArgsRefusal(tool, choice);
50
+ if (refusal !== null)
51
+ throw new Error(refusal);
52
+ // A set field got past the refusal, so `tool` is in EFFORT_ARGV.
53
+ return [
54
+ ...(choice.via ?? []),
55
+ ...(choice.model !== undefined ? modelFlag(choice.model) : []),
56
+ ...(choice.effort !== undefined ? EFFORT_ARGV[tool](choice.effort) : []),
57
+ ];
58
+ }
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Reasoning-effort intent, a separate axis from model choice: the closed set
3
+ * the adapters translate. Maps to claude `--effort`, codex
4
+ * `model_reasoning_effort`, antigravity `--effort` (clamped to `high`), opencode
5
+ * `--variant`. Frozen at run time.
6
+ */
7
+ export declare const EFFORT_LEVELS: readonly ["low", "medium", "high", "xhigh", "max"];
8
+ /** One of {@link EFFORT_LEVELS}. */
9
+ export type EffortLevel = (typeof EFFORT_LEVELS)[number];
package/dist/models.js ADDED
@@ -0,0 +1,12 @@
1
+ // Node-free entry point (the package's `./models` subpath export): the effort vocabulary.
2
+ // Imports nothing, so a browser bundle can take it without pulling in the
3
+ // process runner (`node:child_process`) that the package barrel re-exports.
4
+ // This package takes a resolved model; which levels a model accepts is not
5
+ // decided here.
6
+ /**
7
+ * Reasoning-effort intent, a separate axis from model choice: the closed set
8
+ * the adapters translate. Maps to claude `--effort`, codex
9
+ * `model_reasoning_effort`, antigravity `--effort` (clamped to `high`), opencode
10
+ * `--variant`. Frozen at run time.
11
+ */
12
+ export const EFFORT_LEVELS = Object.freeze(['low', 'medium', 'high', 'xhigh', 'max']);
package/dist/plan.d.ts ADDED
@@ -0,0 +1,56 @@
1
+ import type { AgentCallSpec, OutputFormat } from './spec.js';
2
+ /**
3
+ * How faithfully a spec field survived translation to a concrete CLI call.
4
+ *
5
+ * - `native` — the CLI has a first-class flag for it.
6
+ * - `approximated` — mapped to the closest available behavior.
7
+ * - `emulated` — reproduced through prompt engineering or env plumbing.
8
+ * - `dropped` — the CLI has no way to express it; the field was ignored.
9
+ */
10
+ export type Support = 'native' | 'approximated' | 'emulated' | 'dropped';
11
+ /** How one spec field was translated for one CLI, and why. */
12
+ export interface CapabilityNote {
13
+ /** Dotted path into the spec, e.g. `"permissions.mode"`. */
14
+ field: string;
15
+ support: Support;
16
+ detail: string;
17
+ }
18
+ /**
19
+ * Placeholder that adapters embed in argv/env values wherever a temp-file
20
+ * path is needed. The runner replaces it with a real per-call temp directory
21
+ * after materializing `PlannedCall.tempFiles`.
22
+ */
23
+ export declare const TMP_TOKEN = "__AGENT_CALL_TMP__";
24
+ /** A file the runner writes into the per-call temp directory before the spawn. */
25
+ export interface TempFile {
26
+ /** File name inside the per-call temp directory. */
27
+ name: string;
28
+ /** Content written before spawn. Empty string reserves the path only. */
29
+ content: string;
30
+ /** Read the file back after the run (exposed as `RawProcessOutput.files`). */
31
+ readBack?: boolean | undefined;
32
+ }
33
+ /**
34
+ * A fully translated call: pure data, no side effects. `planCall()` produces
35
+ * one without running anything, so you can inspect, log, or golden-test the
36
+ * exact invocation an adapter would make.
37
+ */
38
+ export interface PlannedCall {
39
+ agent: string;
40
+ /** argv[0] is the binary. */
41
+ argv: string[];
42
+ /** Written to the child's stdin when set (how most prompts travel). */
43
+ stdin?: string | undefined;
44
+ /** The prompt when it travels in argv rather than stdin, kept so a conversation can report it. */
45
+ prompt?: string | undefined;
46
+ /** Environment additions (merged over the parent env by the runner). */
47
+ env: Record<string, string>;
48
+ cwd?: string | undefined;
49
+ /** The output format actually requested from the CLI. */
50
+ outputFormat: OutputFormat;
51
+ tempFiles: TempFile[];
52
+ notes: CapabilityNote[];
53
+ /** The canonical spec this call was planned from. */
54
+ spec: AgentCallSpec;
55
+ }
56
+ export declare function note(notes: CapabilityNote[], field: string, support: Support, detail: string): void;
package/dist/plan.js ADDED
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Placeholder that adapters embed in argv/env values wherever a temp-file
3
+ * path is needed. The runner replaces it with a real per-call temp directory
4
+ * after materializing `PlannedCall.tempFiles`.
5
+ */
6
+ export const TMP_TOKEN = '__AGENT_CALL_TMP__';
7
+ export function note(notes, field, support, detail) {
8
+ notes.push({ field, support, detail });
9
+ }