specrails-core 6.4.0 → 6.5.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/dist/agent-runtime/capabilities.js +22 -5
- package/dist/agent-runtime/cli-executor.js +8 -3
- package/dist/agent-runtime/cli.d.ts +1 -1
- package/dist/agent-runtime/cli.js +50 -3
- package/dist/agent-runtime/compact/environment.d.ts +30 -2
- package/dist/agent-runtime/compact/environment.js +120 -2
- package/dist/agent-runtime/engine/pieces/role-turn.d.ts +5 -0
- package/dist/agent-runtime/engine/pieces/role-turn.js +54 -21
- package/dist/agent-runtime/engine/pieces/verify.js +3 -5
- package/dist/agent-runtime/session/application/active-session.d.ts +176 -0
- package/dist/agent-runtime/session/application/active-session.js +701 -0
- package/dist/agent-runtime/session/application/delegation.d.ts +73 -0
- package/dist/agent-runtime/session/application/delegation.js +171 -0
- package/dist/agent-runtime/session/application/output-buffer.d.ts +43 -0
- package/dist/agent-runtime/session/application/output-buffer.js +65 -0
- package/dist/agent-runtime/session/application/session-service.d.ts +121 -0
- package/dist/agent-runtime/session/application/session-service.js +256 -0
- package/dist/agent-runtime/session/application/timers.d.ts +15 -0
- package/dist/agent-runtime/session/application/timers.js +35 -0
- package/dist/agent-runtime/session/domain/descriptor.d.ts +6 -0
- package/dist/agent-runtime/session/domain/descriptor.js +32 -0
- package/dist/agent-runtime/session/domain/errors.d.ts +9 -0
- package/dist/agent-runtime/session/domain/errors.js +16 -0
- package/dist/agent-runtime/session/domain/events.d.ts +147 -0
- package/dist/agent-runtime/session/domain/events.js +24 -0
- package/dist/agent-runtime/session/domain/fingerprint.d.ts +2 -0
- package/dist/agent-runtime/session/domain/fingerprint.js +10 -0
- package/dist/agent-runtime/session/domain/interruption.d.ts +13 -0
- package/dist/agent-runtime/session/domain/interruption.js +39 -0
- package/dist/agent-runtime/session/domain/policy.d.ts +42 -0
- package/dist/agent-runtime/session/domain/policy.js +104 -0
- package/dist/agent-runtime/session/domain/snapshot.d.ts +100 -0
- package/dist/agent-runtime/session/domain/snapshot.js +234 -0
- package/dist/agent-runtime/session/domain/state-machines.d.ts +21 -0
- package/dist/agent-runtime/session/domain/state-machines.js +41 -0
- package/dist/agent-runtime/session/domain/types.d.ts +130 -0
- package/dist/agent-runtime/session/domain/types.js +15 -0
- package/dist/agent-runtime/session/domain/usage.d.ts +54 -0
- package/dist/agent-runtime/session/domain/usage.js +86 -0
- package/dist/agent-runtime/session/drivers/claude/argv.d.ts +19 -0
- package/dist/agent-runtime/session/drivers/claude/argv.js +67 -0
- package/dist/agent-runtime/session/drivers/claude/driver.d.ts +19 -0
- package/dist/agent-runtime/session/drivers/claude/driver.js +134 -0
- package/dist/agent-runtime/session/drivers/claude/translator.d.ts +40 -0
- package/dist/agent-runtime/session/drivers/claude/translator.js +247 -0
- package/dist/agent-runtime/session/drivers/codex/argv.d.ts +12 -0
- package/dist/agent-runtime/session/drivers/codex/argv.js +76 -0
- package/dist/agent-runtime/session/drivers/codex/driver.d.ts +19 -0
- package/dist/agent-runtime/session/drivers/codex/driver.js +193 -0
- package/dist/agent-runtime/session/drivers/codex/rpc.d.ts +28 -0
- package/dist/agent-runtime/session/drivers/codex/rpc.js +81 -0
- package/dist/agent-runtime/session/drivers/codex/translator.d.ts +53 -0
- package/dist/agent-runtime/session/drivers/codex/translator.js +267 -0
- package/dist/agent-runtime/session/drivers/common/process.d.ts +37 -0
- package/dist/agent-runtime/session/drivers/common/process.js +116 -0
- package/dist/agent-runtime/session/drivers/executor/driver.d.ts +22 -0
- package/dist/agent-runtime/session/drivers/executor/driver.js +163 -0
- package/dist/agent-runtime/session/drivers/registry.d.ts +32 -0
- package/dist/agent-runtime/session/drivers/registry.js +38 -0
- package/dist/agent-runtime/session/host/protocol.d.ts +18 -0
- package/dist/agent-runtime/session/host/protocol.js +88 -0
- package/dist/agent-runtime/session/host/server.d.ts +57 -0
- package/dist/agent-runtime/session/host/server.js +229 -0
- package/dist/agent-runtime/session/index.d.ts +45 -0
- package/dist/agent-runtime/session/index.js +74 -0
- package/dist/agent-runtime/session/journal/schema.d.ts +7 -0
- package/dist/agent-runtime/session/journal/schema.js +44 -0
- package/dist/agent-runtime/session/journal/sqlite-journal.d.ts +70 -0
- package/dist/agent-runtime/session/journal/sqlite-journal.js +279 -0
- package/dist/agent-runtime/session/ports.d.ts +192 -0
- package/dist/agent-runtime/session/ports.js +1 -0
- package/dist/agent-runtime/session/testing/driver-conformance.d.ts +21 -0
- package/dist/agent-runtime/session/testing/driver-conformance.js +97 -0
- package/dist/agent-runtime/session/testing/fake-clock.d.ts +20 -0
- package/dist/agent-runtime/session/testing/fake-clock.js +34 -0
- package/dist/agent-runtime/session/testing/fixture-replayer.d.ts +41 -0
- package/dist/agent-runtime/session/testing/fixture-replayer.js +172 -0
- package/dist/agent-runtime/session/testing/index.d.ts +9 -0
- package/dist/agent-runtime/session/testing/index.js +9 -0
- package/dist/agent-runtime/session/testing/memory-journal.d.ts +27 -0
- package/dist/agent-runtime/session/testing/memory-journal.js +70 -0
- package/dist/agent-runtime/session/testing/scripted-driver.d.ts +30 -0
- package/dist/agent-runtime/session/testing/scripted-driver.js +77 -0
- package/dist/agent-runtime/verification-proposals.d.ts +9 -0
- package/dist/agent-runtime/verification-proposals.js +69 -0
- package/dist/installer/util/registry.js +3 -3
- package/dist/shared/specrails-home.d.ts +21 -0
- package/dist/shared/specrails-home.js +36 -0
- package/integration-contract.json +42 -1
- package/package.json +11 -1
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { type Usage, type UsageSemantics } from './types.js';
|
|
2
|
+
/** Raw values as a provider reported them for one turn (or one sub-agent thread). */
|
|
3
|
+
export interface ReportedUsage {
|
|
4
|
+
inputTokens?: number | null;
|
|
5
|
+
outputTokens?: number | null;
|
|
6
|
+
cacheReadTokens?: number | null;
|
|
7
|
+
cacheWriteTokens?: number | null;
|
|
8
|
+
totalTokens?: number | null;
|
|
9
|
+
costUsd?: number | null;
|
|
10
|
+
model?: string | null;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Last known cumulative provider totals for one provider session (or thread).
|
|
14
|
+
* `null` means unknown — e.g. a session resumed from history we never observed —
|
|
15
|
+
* which is different from a fresh session whose baseline is 0.
|
|
16
|
+
*/
|
|
17
|
+
export interface UsageBaseline {
|
|
18
|
+
costUsd: number | null;
|
|
19
|
+
inputTokens: number | null;
|
|
20
|
+
outputTokens: number | null;
|
|
21
|
+
cacheReadTokens: number | null;
|
|
22
|
+
cacheWriteTokens: number | null;
|
|
23
|
+
}
|
|
24
|
+
export declare const FRESH_BASELINE: UsageBaseline;
|
|
25
|
+
export declare const UNKNOWN_BASELINE: UsageBaseline;
|
|
26
|
+
export interface TurnUsageResult {
|
|
27
|
+
usage: Usage;
|
|
28
|
+
baseline: UsageBaseline;
|
|
29
|
+
/** Values that went backwards (provider reset); the reported value was used as the delta. */
|
|
30
|
+
resets: string[];
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Convert what the provider reported into this turn's usage, according to the
|
|
34
|
+
* driver's declared semantics. Verified against real CLIs:
|
|
35
|
+
* - Claude reports USD cumulative per session, including across `--resume`,
|
|
36
|
+
* and tokens per turn.
|
|
37
|
+
* - Codex reports tokens cumulative per thread and no USD.
|
|
38
|
+
* Missing values stay null; nothing is ever coerced to zero.
|
|
39
|
+
*/
|
|
40
|
+
export declare function computeTurnUsage(semantics: UsageSemantics, baseline: UsageBaseline, reported: ReportedUsage): TurnUsageResult;
|
|
41
|
+
/** USD per million tokens; supplied by the host, never guessed by Core. */
|
|
42
|
+
export interface RateCard {
|
|
43
|
+
inputPerMTok: number;
|
|
44
|
+
outputPerMTok: number;
|
|
45
|
+
cacheReadPerMTok?: number;
|
|
46
|
+
cacheWritePerMTok?: number;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Fill a missing cost from a rate card and flag it as an estimate. A billed cost
|
|
50
|
+
* is never overwritten; usage with no billable tokens stays null.
|
|
51
|
+
*/
|
|
52
|
+
export declare function withEstimatedCost(usage: Usage, rate: RateCard | null): Usage;
|
|
53
|
+
/** Sum usages for display breakdowns; a sum is null only when every part is null. */
|
|
54
|
+
export declare function sumUsage(items: readonly Usage[]): Usage;
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { EMPTY_USAGE } from './types.js';
|
|
2
|
+
const COUNTERS = ['inputTokens', 'outputTokens', 'cacheReadTokens', 'cacheWriteTokens'];
|
|
3
|
+
export const FRESH_BASELINE = Object.freeze({ costUsd: 0, inputTokens: 0, outputTokens: 0, cacheReadTokens: 0, cacheWriteTokens: 0 });
|
|
4
|
+
export const UNKNOWN_BASELINE = Object.freeze({ costUsd: null, inputTokens: null, outputTokens: null, cacheReadTokens: null, cacheWriteTokens: null });
|
|
5
|
+
function finite(value) {
|
|
6
|
+
return typeof value === 'number' && Number.isFinite(value) && value >= 0 ? value : null;
|
|
7
|
+
}
|
|
8
|
+
/** Delta of a cumulative counter against its baseline. Unknown baseline → unknown delta. */
|
|
9
|
+
function delta(reported, base, key, resets) {
|
|
10
|
+
if (reported === null)
|
|
11
|
+
return { value: null, next: base };
|
|
12
|
+
if (base === null)
|
|
13
|
+
return { value: null, next: reported };
|
|
14
|
+
if (reported < base) {
|
|
15
|
+
resets.push(key);
|
|
16
|
+
return { value: reported, next: reported };
|
|
17
|
+
}
|
|
18
|
+
return { value: reported - base, next: reported };
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Convert what the provider reported into this turn's usage, according to the
|
|
22
|
+
* driver's declared semantics. Verified against real CLIs:
|
|
23
|
+
* - Claude reports USD cumulative per session, including across `--resume`,
|
|
24
|
+
* and tokens per turn.
|
|
25
|
+
* - Codex reports tokens cumulative per thread and no USD.
|
|
26
|
+
* Missing values stay null; nothing is ever coerced to zero.
|
|
27
|
+
*/
|
|
28
|
+
export function computeTurnUsage(semantics, baseline, reported) {
|
|
29
|
+
const resets = [];
|
|
30
|
+
const next = { ...baseline };
|
|
31
|
+
const usage = { ...EMPTY_USAGE, model: reported.model ?? null };
|
|
32
|
+
// A reported total is only meaningful per turn; cumulative totals are not tracked as a baseline.
|
|
33
|
+
if (semantics.tokens === 'per-turn')
|
|
34
|
+
usage.totalTokens = finite(reported.totalTokens);
|
|
35
|
+
if (semantics.costUsd === 'session-cumulative') {
|
|
36
|
+
const result = delta(finite(reported.costUsd), baseline.costUsd, 'costUsd', resets);
|
|
37
|
+
usage.costUsd = result.value;
|
|
38
|
+
next.costUsd = result.next;
|
|
39
|
+
}
|
|
40
|
+
else if (semantics.costUsd === 'per-turn') {
|
|
41
|
+
usage.costUsd = finite(reported.costUsd);
|
|
42
|
+
}
|
|
43
|
+
for (const key of COUNTERS) {
|
|
44
|
+
if (semantics.tokens === 'per-turn') {
|
|
45
|
+
usage[key] = finite(reported[key]);
|
|
46
|
+
}
|
|
47
|
+
else if (semantics.tokens === 'cumulative') {
|
|
48
|
+
const result = delta(finite(reported[key]), baseline[key], key, resets);
|
|
49
|
+
usage[key] = result.value;
|
|
50
|
+
next[key] = result.next;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
return { usage, baseline: next, resets };
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Fill a missing cost from a rate card and flag it as an estimate. A billed cost
|
|
57
|
+
* is never overwritten; usage with no billable tokens stays null.
|
|
58
|
+
*/
|
|
59
|
+
export function withEstimatedCost(usage, rate) {
|
|
60
|
+
if (usage.costUsd !== null || !rate)
|
|
61
|
+
return usage;
|
|
62
|
+
const parts = [
|
|
63
|
+
[usage.inputTokens, rate.inputPerMTok],
|
|
64
|
+
[usage.outputTokens, rate.outputPerMTok],
|
|
65
|
+
[usage.cacheReadTokens, rate.cacheReadPerMTok],
|
|
66
|
+
[usage.cacheWriteTokens, rate.cacheWritePerMTok],
|
|
67
|
+
];
|
|
68
|
+
if (!parts.some(([tokens]) => (tokens ?? 0) > 0))
|
|
69
|
+
return usage;
|
|
70
|
+
const cost = parts.reduce((sum, [tokens, price]) => sum + ((tokens ?? 0) * (price ?? 0)) / 1_000_000, 0);
|
|
71
|
+
return { ...usage, costUsd: cost, costEstimated: true };
|
|
72
|
+
}
|
|
73
|
+
/** Sum usages for display breakdowns; a sum is null only when every part is null. */
|
|
74
|
+
export function sumUsage(items) {
|
|
75
|
+
const total = { ...EMPTY_USAGE };
|
|
76
|
+
for (const item of items) {
|
|
77
|
+
for (const key of [...COUNTERS, 'totalTokens', 'costUsd']) {
|
|
78
|
+
const value = item[key];
|
|
79
|
+
if (value !== null)
|
|
80
|
+
total[key] = (total[key] ?? 0) + value;
|
|
81
|
+
}
|
|
82
|
+
total.costEstimated = total.costEstimated || item.costEstimated;
|
|
83
|
+
total.model = total.model ?? item.model;
|
|
84
|
+
}
|
|
85
|
+
return total;
|
|
86
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { DriverOpenSpec } from '../../ports.js';
|
|
2
|
+
/** Tools that launch sub-agents in Claude Code; removed when policy disables sub-agents. */
|
|
3
|
+
export declare const CLAUDE_SUBAGENT_TOOLS: readonly ["Agent", "Task"];
|
|
4
|
+
export declare const CLAUDE_READ_ONLY_TOOLS: readonly ["Read", "Grep", "Glob"];
|
|
5
|
+
/**
|
|
6
|
+
* Map an open spec + policy to Claude Code argv. Lists are passed as one
|
|
7
|
+
* comma-separated argument (variadic flags would otherwise swallow later args).
|
|
8
|
+
* `systemPromptFile` is used instead of an inline prompt when provided
|
|
9
|
+
* (long prompts / Windows command-line limits).
|
|
10
|
+
*/
|
|
11
|
+
export declare function claudeArgs(spec: DriverOpenSpec, options?: {
|
|
12
|
+
systemPromptFile?: string;
|
|
13
|
+
}): string[];
|
|
14
|
+
/**
|
|
15
|
+
* Environment for the Claude process. Native sub-agents run on another model
|
|
16
|
+
* through `CLAUDE_CODE_SUBAGENT_MODEL` (verified live: a haiku parent ran a
|
|
17
|
+
* sonnet sub-agent). Claude has no per-sub-agent effort.
|
|
18
|
+
*/
|
|
19
|
+
export declare function claudeEnv(policy: DriverOpenSpec['policy'], base: NodeJS.ProcessEnv): NodeJS.ProcessEnv;
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { nativeSubagentsAllowed } from '../../domain/policy.js';
|
|
2
|
+
/** Tools that launch sub-agents in Claude Code; removed when policy disables sub-agents. */
|
|
3
|
+
export const CLAUDE_SUBAGENT_TOOLS = ['Agent', 'Task'];
|
|
4
|
+
export const CLAUDE_READ_ONLY_TOOLS = ['Read', 'Grep', 'Glob'];
|
|
5
|
+
function mcpConfig(servers) {
|
|
6
|
+
return JSON.stringify({
|
|
7
|
+
mcpServers: Object.fromEntries(servers.map((server) => [server.name, server.url
|
|
8
|
+
? { type: 'http', url: server.url, ...(server.headers ? { headers: server.headers } : {}) }
|
|
9
|
+
: { command: server.command, args: server.args ?? [], ...(server.env ? { env: server.env } : {}) }])),
|
|
10
|
+
});
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Map an open spec + policy to Claude Code argv. Lists are passed as one
|
|
14
|
+
* comma-separated argument (variadic flags would otherwise swallow later args).
|
|
15
|
+
* `systemPromptFile` is used instead of an inline prompt when provided
|
|
16
|
+
* (long prompts / Windows command-line limits).
|
|
17
|
+
*/
|
|
18
|
+
export function claudeArgs(spec, options = {}) {
|
|
19
|
+
const policy = spec.policy;
|
|
20
|
+
const args = [
|
|
21
|
+
'-p',
|
|
22
|
+
'--input-format', 'stream-json',
|
|
23
|
+
'--output-format', 'stream-json',
|
|
24
|
+
'--verbose',
|
|
25
|
+
'--replay-user-messages',
|
|
26
|
+
'--setting-sources', 'project,local',
|
|
27
|
+
'--model', spec.model,
|
|
28
|
+
];
|
|
29
|
+
if (spec.effort)
|
|
30
|
+
args.push('--effort', spec.effort);
|
|
31
|
+
if (options.systemPromptFile)
|
|
32
|
+
args.push('--system-prompt-file', options.systemPromptFile);
|
|
33
|
+
else if (spec.systemPrompt)
|
|
34
|
+
args.push('--system-prompt', spec.systemPrompt);
|
|
35
|
+
if (policy.permissions === 'bypass')
|
|
36
|
+
args.push('--dangerously-skip-permissions');
|
|
37
|
+
else
|
|
38
|
+
args.push('--permission-mode', policy.permissions === 'read-only' ? 'plan' : 'acceptEdits');
|
|
39
|
+
if (policy.tools.mode === 'none')
|
|
40
|
+
args.push('--tools', '');
|
|
41
|
+
else if (policy.tools.mode === 'read-only')
|
|
42
|
+
args.push('--tools', CLAUDE_READ_ONLY_TOOLS.join(','));
|
|
43
|
+
if (policy.tools.allow?.length)
|
|
44
|
+
args.push('--allowedTools', policy.tools.allow.join(','));
|
|
45
|
+
const denied = [...new Set([...(policy.tools.deny ?? []), ...(!nativeSubagentsAllowed(policy) ? CLAUDE_SUBAGENT_TOOLS : [])])];
|
|
46
|
+
if (denied.length)
|
|
47
|
+
args.push('--disallowedTools', denied.join(','));
|
|
48
|
+
if (policy.mcp.servers.length)
|
|
49
|
+
args.push('--mcp-config', mcpConfig(policy.mcp.servers));
|
|
50
|
+
if (!policy.mcp.inheritUserScope)
|
|
51
|
+
args.push('--strict-mcp-config');
|
|
52
|
+
if (spec.providerSessionRef)
|
|
53
|
+
args.push('--resume', spec.providerSessionRef);
|
|
54
|
+
return args;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Environment for the Claude process. Native sub-agents run on another model
|
|
58
|
+
* through `CLAUDE_CODE_SUBAGENT_MODEL` (verified live: a haiku parent ran a
|
|
59
|
+
* sonnet sub-agent). Claude has no per-sub-agent effort.
|
|
60
|
+
*/
|
|
61
|
+
export function claudeEnv(policy, base) {
|
|
62
|
+
const env = { ...base };
|
|
63
|
+
delete env.CLAUDE_CODE_SUBAGENT_MODEL;
|
|
64
|
+
if (nativeSubagentsAllowed(policy) && policy.subagentRuntime.mode === 'native' && policy.subagentRuntime.model)
|
|
65
|
+
env.CLAUDE_CODE_SUBAGENT_MODEL = policy.subagentRuntime.model;
|
|
66
|
+
return env;
|
|
67
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { DriverDescriptor } from '../../domain/types.js';
|
|
2
|
+
import type { DriverEventSink, DriverFactory, DriverOpenSpec, DriverSession } from '../../ports.js';
|
|
3
|
+
import { type ProcessSpawner } from '../common/process.js';
|
|
4
|
+
export declare const CLAUDE_DESCRIPTOR: DriverDescriptor;
|
|
5
|
+
export interface ClaudeDriverOptions {
|
|
6
|
+
binary?: string;
|
|
7
|
+
spawner?: ProcessSpawner;
|
|
8
|
+
env?: NodeJS.ProcessEnv;
|
|
9
|
+
/** Grace between SIGTERM and SIGKILL; Claude reports stopped tasks while it shuts down. */
|
|
10
|
+
terminateGraceMs?: number;
|
|
11
|
+
/** Inline system prompts longer than this go through a private temp file. */
|
|
12
|
+
maxInlineSystemPrompt?: number;
|
|
13
|
+
}
|
|
14
|
+
export declare class ClaudeDriverFactory implements DriverFactory {
|
|
15
|
+
private readonly options;
|
|
16
|
+
readonly descriptor: DriverDescriptor;
|
|
17
|
+
constructor(options?: ClaudeDriverOptions);
|
|
18
|
+
open(spec: DriverOpenSpec, sink: DriverEventSink): Promise<DriverSession>;
|
|
19
|
+
}
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import { mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
|
|
3
|
+
import { tmpdir } from 'node:os';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import { spawnResidentProcess } from '../common/process.js';
|
|
6
|
+
import { claudeArgs, claudeEnv } from './argv.js';
|
|
7
|
+
import { ClaudeTranslator } from './translator.js';
|
|
8
|
+
export const CLAUDE_DESCRIPTOR = Object.freeze({
|
|
9
|
+
id: 'claude',
|
|
10
|
+
displayName: 'Claude Code',
|
|
11
|
+
testedVersions: ['2.1.285'],
|
|
12
|
+
capabilities: Object.freeze({
|
|
13
|
+
resident: true,
|
|
14
|
+
nativeInputQueue: true,
|
|
15
|
+
subagents: 'supported',
|
|
16
|
+
subagentDisable: true,
|
|
17
|
+
subagentModel: true,
|
|
18
|
+
subagentEffort: false,
|
|
19
|
+
autonomousContinuation: true,
|
|
20
|
+
steer: true,
|
|
21
|
+
toolFiltering: true,
|
|
22
|
+
usage: Object.freeze({ costUsd: 'session-cumulative', tokens: 'per-turn' }),
|
|
23
|
+
}),
|
|
24
|
+
});
|
|
25
|
+
const IMAGE_TYPES = Object.freeze({ '.png': 'image/png', '.jpg': 'image/jpeg', '.jpeg': 'image/jpeg', '.gif': 'image/gif', '.webp': 'image/webp' });
|
|
26
|
+
function userContent(input) {
|
|
27
|
+
const files = (input.attachments ?? []).filter((item) => item.kind === 'file' || !IMAGE_TYPES[path.extname(item.path).toLowerCase()]);
|
|
28
|
+
const images = (input.attachments ?? []).filter((item) => item.kind === 'image' && IMAGE_TYPES[path.extname(item.path).toLowerCase()]);
|
|
29
|
+
const text = files.length ? `${input.text}\n\nAttached files:\n${files.map((file) => `- ${file.path}`).join('\n')}` : input.text;
|
|
30
|
+
if (images.length === 0)
|
|
31
|
+
return text;
|
|
32
|
+
return [
|
|
33
|
+
...images.map((image) => ({ type: 'image', source: { type: 'base64', media_type: image.mimeType ?? IMAGE_TYPES[path.extname(image.path).toLowerCase()], data: readFileSync(image.path).toString('base64') } })),
|
|
34
|
+
{ type: 'text', text },
|
|
35
|
+
];
|
|
36
|
+
}
|
|
37
|
+
class ClaudeDriverSession {
|
|
38
|
+
process;
|
|
39
|
+
translator;
|
|
40
|
+
graceMs;
|
|
41
|
+
cleanup;
|
|
42
|
+
closing = null;
|
|
43
|
+
constructor(process, translator, graceMs, cleanup) {
|
|
44
|
+
this.process = process;
|
|
45
|
+
this.translator = translator;
|
|
46
|
+
this.graceMs = graceMs;
|
|
47
|
+
this.cleanup = cleanup;
|
|
48
|
+
}
|
|
49
|
+
async send(input) {
|
|
50
|
+
this.translator.noteInput(input.inputId);
|
|
51
|
+
const frame = {
|
|
52
|
+
type: 'user',
|
|
53
|
+
uuid: input.inputId,
|
|
54
|
+
session_id: '',
|
|
55
|
+
parent_tool_use_id: null,
|
|
56
|
+
// `next` delivers into the running turn; otherwise the CLI queues it for the next turn.
|
|
57
|
+
...(input.delivery === 'steer' ? { priority: 'next' } : {}),
|
|
58
|
+
message: { role: 'user', content: userContent(input) },
|
|
59
|
+
};
|
|
60
|
+
if (!this.process.write(JSON.stringify(frame)))
|
|
61
|
+
throw new Error('Claude input is closed');
|
|
62
|
+
}
|
|
63
|
+
async interrupt() {
|
|
64
|
+
this.translator.noteInterrupt();
|
|
65
|
+
this.process.write(JSON.stringify({ type: 'control_request', request_id: randomUUID(), request: { subtype: 'interrupt' } }));
|
|
66
|
+
}
|
|
67
|
+
/** Claude Code exposes no per-task stop on stdin: stopping means ending the process. */
|
|
68
|
+
async stopSubagents() {
|
|
69
|
+
return 'process';
|
|
70
|
+
}
|
|
71
|
+
close(_reason) {
|
|
72
|
+
this.closing ??= (async () => {
|
|
73
|
+
this.process.endInput();
|
|
74
|
+
await this.process.terminate(this.graceMs);
|
|
75
|
+
this.cleanup();
|
|
76
|
+
})();
|
|
77
|
+
return this.closing;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
export class ClaudeDriverFactory {
|
|
81
|
+
options;
|
|
82
|
+
descriptor = CLAUDE_DESCRIPTOR;
|
|
83
|
+
constructor(options = {}) {
|
|
84
|
+
this.options = options;
|
|
85
|
+
}
|
|
86
|
+
async open(spec, sink) {
|
|
87
|
+
const translator = new ClaudeTranslator();
|
|
88
|
+
let promptDir = null;
|
|
89
|
+
let systemPromptFile;
|
|
90
|
+
const maxInline = this.options.maxInlineSystemPrompt ?? (process.platform === 'win32' ? 4_000 : 64_000);
|
|
91
|
+
if (spec.systemPrompt && spec.systemPrompt.length > maxInline) {
|
|
92
|
+
promptDir = mkdtempSync(path.join(tmpdir(), 'specrails-session-'));
|
|
93
|
+
systemPromptFile = path.join(promptDir, 'system-prompt.md');
|
|
94
|
+
writeFileSync(systemPromptFile, spec.systemPrompt, { mode: 0o600 });
|
|
95
|
+
}
|
|
96
|
+
const cleanup = () => { if (promptDir)
|
|
97
|
+
rmSync(promptDir, { recursive: true, force: true }); };
|
|
98
|
+
let closed = false;
|
|
99
|
+
const spawner = this.options.spawner ?? spawnResidentProcess;
|
|
100
|
+
const child = spawner({ command: this.options.binary ?? 'claude', args: claudeArgs(spec, systemPromptFile ? { systemPromptFile } : {}), cwd: spec.cwd, env: claudeEnv(spec.policy, this.options.env ?? process.env) }, {
|
|
101
|
+
onLine: (line) => {
|
|
102
|
+
if (closed)
|
|
103
|
+
return;
|
|
104
|
+
let frame;
|
|
105
|
+
try {
|
|
106
|
+
frame = JSON.parse(line);
|
|
107
|
+
}
|
|
108
|
+
catch {
|
|
109
|
+
sink({ kind: 'diagnostic', level: 'warning', code: 'provider.unparseable', message: line.slice(0, 500) });
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
for (const event of translator.translate(frame))
|
|
113
|
+
sink(event);
|
|
114
|
+
},
|
|
115
|
+
});
|
|
116
|
+
const session = new ClaudeDriverSession(child, translator, this.options.terminateGraceMs ?? 5_000, cleanup);
|
|
117
|
+
let closeRequested = false;
|
|
118
|
+
void child.exited.then((exit) => {
|
|
119
|
+
cleanup();
|
|
120
|
+
// An exit we asked for is not a crash; lines printed while stopping were already delivered.
|
|
121
|
+
if (!closed && !closeRequested)
|
|
122
|
+
sink({ kind: 'process.exited', exitCode: exit.exitCode, signal: exit.signal });
|
|
123
|
+
closed = true;
|
|
124
|
+
});
|
|
125
|
+
const originalClose = session.close.bind(session);
|
|
126
|
+
session.close = async (reason) => {
|
|
127
|
+
closeRequested = true;
|
|
128
|
+
await originalClose(reason);
|
|
129
|
+
closed = true;
|
|
130
|
+
};
|
|
131
|
+
sink({ kind: 'process.started', ...(child.pid !== undefined ? { pid: child.pid } : {}) });
|
|
132
|
+
return session;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import type { DriverEvent } from '../../ports.js';
|
|
2
|
+
/**
|
|
3
|
+
* Anti-corruption layer for Claude Code `--output-format stream-json`.
|
|
4
|
+
* Deterministic and free of I/O: one wire frame in, normalized events out.
|
|
5
|
+
* Behaviour pinned by the recorded transcripts (Claude Code 2.1.285):
|
|
6
|
+
* - `command_lifecycle` = per-input receipts; the CLI serializes turns itself;
|
|
7
|
+
* - a turn runs from `system/init` to `result`; turns without a started input
|
|
8
|
+
* are the CLI's own continuations (result `origin.kind = task-notification`);
|
|
9
|
+
* - sub-agents and their shells are `task_*` frames; their messages carry
|
|
10
|
+
* `parent_tool_use_id`; the same task id restarts after reporting completion.
|
|
11
|
+
*/
|
|
12
|
+
type Frame = Record<string, unknown>;
|
|
13
|
+
export declare class ClaudeTranslator {
|
|
14
|
+
private sessionRef;
|
|
15
|
+
private turnOpen;
|
|
16
|
+
private interruptRequested;
|
|
17
|
+
private readonly written;
|
|
18
|
+
private startedInputs;
|
|
19
|
+
/** tool_use id → task id of the agent that issued it (null = main agent). */
|
|
20
|
+
private readonly toolOwner;
|
|
21
|
+
private readonly toolName;
|
|
22
|
+
/** Agent/Task tool_use id → task id it became. */
|
|
23
|
+
private readonly taskByToolUse;
|
|
24
|
+
private readonly taskPhase;
|
|
25
|
+
private readonly reportedUnknown;
|
|
26
|
+
/** The driver wrote this input; its receipts and replays belong to us. */
|
|
27
|
+
noteInput(inputId: string): void;
|
|
28
|
+
noteInterrupt(): void;
|
|
29
|
+
translate(frame: Frame): DriverEvent[];
|
|
30
|
+
private system;
|
|
31
|
+
private taskStarted;
|
|
32
|
+
private phase;
|
|
33
|
+
private lifecycle;
|
|
34
|
+
private ownerOf;
|
|
35
|
+
private assistant;
|
|
36
|
+
private user;
|
|
37
|
+
private result;
|
|
38
|
+
private unknown;
|
|
39
|
+
}
|
|
40
|
+
export {};
|
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
const TASK_PHASE = Object.freeze({
|
|
2
|
+
completed: 'idle',
|
|
3
|
+
failed: 'failed',
|
|
4
|
+
stopped: 'stopped',
|
|
5
|
+
killed: 'killed',
|
|
6
|
+
});
|
|
7
|
+
/** System subtypes that carry nothing the session model needs. */
|
|
8
|
+
const IGNORED_SYSTEM = new Set(['thinking_tokens', 'task_summary', 'post_turn_summary', 'status', 'hook_response', 'hook_started', 'compact_boundary']);
|
|
9
|
+
const IGNORED_TYPES = new Set(['rate_limit_event', 'stream_event', 'keep_alive']);
|
|
10
|
+
function str(value) {
|
|
11
|
+
return typeof value === 'string' ? value : undefined;
|
|
12
|
+
}
|
|
13
|
+
function num(value) {
|
|
14
|
+
return typeof value === 'number' && Number.isFinite(value) ? value : undefined;
|
|
15
|
+
}
|
|
16
|
+
function textOf(content) {
|
|
17
|
+
if (typeof content === 'string')
|
|
18
|
+
return content;
|
|
19
|
+
if (Array.isArray(content))
|
|
20
|
+
return content.map((part) => (part && typeof part === 'object' && part.type === 'text' ? String(part.text ?? '') : '')).join('');
|
|
21
|
+
return '';
|
|
22
|
+
}
|
|
23
|
+
export class ClaudeTranslator {
|
|
24
|
+
sessionRef = null;
|
|
25
|
+
turnOpen = false;
|
|
26
|
+
interruptRequested = false;
|
|
27
|
+
written = new Set();
|
|
28
|
+
startedInputs = [];
|
|
29
|
+
/** tool_use id → task id of the agent that issued it (null = main agent). */
|
|
30
|
+
toolOwner = new Map();
|
|
31
|
+
toolName = new Map();
|
|
32
|
+
/** Agent/Task tool_use id → task id it became. */
|
|
33
|
+
taskByToolUse = new Map();
|
|
34
|
+
taskPhase = new Map();
|
|
35
|
+
reportedUnknown = new Set();
|
|
36
|
+
/** The driver wrote this input; its receipts and replays belong to us. */
|
|
37
|
+
noteInput(inputId) { this.written.add(inputId); }
|
|
38
|
+
noteInterrupt() { if (this.turnOpen)
|
|
39
|
+
this.interruptRequested = true; }
|
|
40
|
+
translate(frame) {
|
|
41
|
+
const type = str(frame.type);
|
|
42
|
+
switch (type) {
|
|
43
|
+
case 'system': return this.system(frame);
|
|
44
|
+
case 'command_lifecycle': return this.lifecycle(frame);
|
|
45
|
+
case 'assistant': return this.assistant(frame);
|
|
46
|
+
case 'user': return this.user(frame);
|
|
47
|
+
case 'result': return this.result(frame);
|
|
48
|
+
default:
|
|
49
|
+
if (type && IGNORED_TYPES.has(type))
|
|
50
|
+
return [];
|
|
51
|
+
return this.unknown(`type:${type ?? '<missing>'}`);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
system(frame) {
|
|
55
|
+
const subtype = str(frame.subtype) ?? '';
|
|
56
|
+
switch (subtype) {
|
|
57
|
+
case 'init': {
|
|
58
|
+
const events = [];
|
|
59
|
+
const ref = str(frame.session_id);
|
|
60
|
+
if (ref && ref !== this.sessionRef) {
|
|
61
|
+
this.sessionRef = ref;
|
|
62
|
+
events.push({ kind: 'provider.ref', providerSessionRef: ref });
|
|
63
|
+
}
|
|
64
|
+
if (!this.turnOpen) {
|
|
65
|
+
this.turnOpen = true;
|
|
66
|
+
const inputIds = this.startedInputs;
|
|
67
|
+
this.startedInputs = [];
|
|
68
|
+
events.push({ kind: 'turn.started', trigger: inputIds.length > 0 ? 'input' : 'continuation', inputIds });
|
|
69
|
+
}
|
|
70
|
+
return events;
|
|
71
|
+
}
|
|
72
|
+
case 'task_started': return this.taskStarted(frame);
|
|
73
|
+
case 'task_progress': {
|
|
74
|
+
const taskId = str(frame.task_id);
|
|
75
|
+
const usage = frame.usage;
|
|
76
|
+
if (!taskId || !this.taskPhase.has(taskId))
|
|
77
|
+
return [];
|
|
78
|
+
return [{
|
|
79
|
+
kind: 'subagent.usage',
|
|
80
|
+
subagentId: taskId,
|
|
81
|
+
usage: { totalTokens: num(usage?.total_tokens) ?? null },
|
|
82
|
+
...(num(usage?.tool_uses) !== undefined ? { toolUses: num(usage?.tool_uses) } : {}),
|
|
83
|
+
...(num(usage?.duration_ms) !== undefined ? { durationMs: num(usage?.duration_ms) } : {}),
|
|
84
|
+
}];
|
|
85
|
+
}
|
|
86
|
+
case 'task_updated': {
|
|
87
|
+
const taskId = str(frame.task_id);
|
|
88
|
+
const status = str(frame.patch?.status);
|
|
89
|
+
return taskId && status ? this.phase(taskId, status) : [];
|
|
90
|
+
}
|
|
91
|
+
case 'task_notification': {
|
|
92
|
+
const taskId = str(frame.task_id);
|
|
93
|
+
const status = str(frame.status);
|
|
94
|
+
if (!taskId || !this.taskPhase.has(taskId))
|
|
95
|
+
return [];
|
|
96
|
+
const usage = frame.usage;
|
|
97
|
+
const summary = str(frame.summary);
|
|
98
|
+
return [
|
|
99
|
+
...(usage ? [{ kind: 'subagent.usage', subagentId: taskId, usage: { totalTokens: num(usage.total_tokens) ?? null }, ...(num(usage.tool_uses) !== undefined ? { toolUses: num(usage.tool_uses) } : {}), ...(num(usage.duration_ms) !== undefined ? { durationMs: num(usage.duration_ms) } : {}) }] : []),
|
|
100
|
+
...(status === 'completed' && summary ? [{ kind: 'subagent.result', subagentId: taskId, summary }] : []),
|
|
101
|
+
...(status ? this.phase(taskId, status) : []),
|
|
102
|
+
];
|
|
103
|
+
}
|
|
104
|
+
case 'background_tasks_changed': {
|
|
105
|
+
const tasks = Array.isArray(frame.tasks) ? frame.tasks : null;
|
|
106
|
+
if (!tasks)
|
|
107
|
+
return [];
|
|
108
|
+
return [{ kind: 'roster', liveSubagentIds: tasks.filter((task) => task?.ambient !== true).map((task) => str(task?.task_id)).filter((id) => !!id) }];
|
|
109
|
+
}
|
|
110
|
+
default:
|
|
111
|
+
return IGNORED_SYSTEM.has(subtype) ? [] : this.unknown(`system:${subtype}`);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
taskStarted(frame) {
|
|
115
|
+
const taskId = str(frame.task_id);
|
|
116
|
+
if (!taskId)
|
|
117
|
+
return [];
|
|
118
|
+
const toolUseId = str(frame.tool_use_id);
|
|
119
|
+
if (toolUseId)
|
|
120
|
+
this.taskByToolUse.set(toolUseId, taskId);
|
|
121
|
+
const restarted = this.taskPhase.has(taskId);
|
|
122
|
+
this.taskPhase.set(taskId, 'running');
|
|
123
|
+
const isShell = frame.task_type === 'local_bash';
|
|
124
|
+
const parentId = toolUseId ? this.toolOwner.get(toolUseId) ?? null : null;
|
|
125
|
+
const prompt = str(frame.prompt);
|
|
126
|
+
return [{
|
|
127
|
+
kind: 'subagent.started',
|
|
128
|
+
subagentId: taskId,
|
|
129
|
+
parentId,
|
|
130
|
+
agentKind: frame.is_backgrounded === true ? 'background' : 'foreground',
|
|
131
|
+
agentType: isShell ? 'shell' : str(frame.subagent_type) ?? 'agent',
|
|
132
|
+
description: str(frame.description) ?? (isShell ? 'Background command' : 'Sub-agent'),
|
|
133
|
+
...(prompt && !restarted ? { prompt } : {}),
|
|
134
|
+
}];
|
|
135
|
+
}
|
|
136
|
+
phase(taskId, status) {
|
|
137
|
+
const phase = TASK_PHASE[status];
|
|
138
|
+
if (!phase || !this.taskPhase.has(taskId) || this.taskPhase.get(taskId) === phase)
|
|
139
|
+
return [];
|
|
140
|
+
this.taskPhase.set(taskId, phase);
|
|
141
|
+
return [{ kind: 'subagent.phase', subagentId: taskId, phase }];
|
|
142
|
+
}
|
|
143
|
+
lifecycle(frame) {
|
|
144
|
+
const inputId = str(frame.command_uuid);
|
|
145
|
+
const state = str(frame.state);
|
|
146
|
+
if (!inputId || !this.written.has(inputId))
|
|
147
|
+
return [];
|
|
148
|
+
if (state === 'started' && !this.turnOpen)
|
|
149
|
+
this.startedInputs.push(inputId);
|
|
150
|
+
if (state === 'queued' || state === 'started' || state === 'completed')
|
|
151
|
+
return [{ kind: 'input.receipt', inputId, state }];
|
|
152
|
+
if (state === 'failed' || state === 'rejected' || state === 'cancelled')
|
|
153
|
+
return [{ kind: 'input.receipt', inputId, state: 'rejected', reason: state }];
|
|
154
|
+
return [];
|
|
155
|
+
}
|
|
156
|
+
ownerOf(frame) {
|
|
157
|
+
const parent = str(frame.parent_tool_use_id);
|
|
158
|
+
if (!parent)
|
|
159
|
+
return null;
|
|
160
|
+
return this.taskByToolUse.get(parent);
|
|
161
|
+
}
|
|
162
|
+
assistant(frame) {
|
|
163
|
+
const owner = this.ownerOf(frame);
|
|
164
|
+
if (owner === undefined)
|
|
165
|
+
return this.unknown('assistant:unattributed-parent');
|
|
166
|
+
const content = (frame.message?.content ?? []);
|
|
167
|
+
const events = [];
|
|
168
|
+
for (const block of Array.isArray(content) ? content : []) {
|
|
169
|
+
if (block.type === 'text' && typeof block.text === 'string' && block.text) {
|
|
170
|
+
events.push(owner === null
|
|
171
|
+
? { kind: 'turn.output', channel: 'text', delta: block.text }
|
|
172
|
+
: { kind: 'subagent.output', subagentId: owner, channel: 'text', delta: block.text });
|
|
173
|
+
}
|
|
174
|
+
else if (block.type === 'thinking' && typeof block.thinking === 'string' && block.thinking && owner === null) {
|
|
175
|
+
events.push({ kind: 'turn.output', channel: 'thinking', delta: block.thinking });
|
|
176
|
+
}
|
|
177
|
+
else if (block.type === 'tool_use') {
|
|
178
|
+
const id = str(block.id);
|
|
179
|
+
const name = str(block.name) ?? 'tool';
|
|
180
|
+
if (!id)
|
|
181
|
+
continue;
|
|
182
|
+
this.toolOwner.set(id, owner);
|
|
183
|
+
this.toolName.set(id, name);
|
|
184
|
+
const tool = { toolUseId: id, name, phase: 'started', input: block.input };
|
|
185
|
+
events.push(owner === null ? { kind: 'turn.tool', ...tool } : { kind: 'subagent.output', subagentId: owner, channel: 'tool', tool });
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
return events;
|
|
189
|
+
}
|
|
190
|
+
user(frame) {
|
|
191
|
+
const uuid = str(frame.uuid);
|
|
192
|
+
if (uuid && this.written.has(uuid))
|
|
193
|
+
return []; // replay of our own input
|
|
194
|
+
const owner = this.ownerOf(frame);
|
|
195
|
+
const content = frame.message?.content;
|
|
196
|
+
if (!Array.isArray(content))
|
|
197
|
+
return [];
|
|
198
|
+
const events = [];
|
|
199
|
+
for (const block of content) {
|
|
200
|
+
if (block.type !== 'tool_result')
|
|
201
|
+
continue;
|
|
202
|
+
const id = str(block.tool_use_id);
|
|
203
|
+
if (!id)
|
|
204
|
+
continue;
|
|
205
|
+
const issuer = this.toolOwner.has(id) ? this.toolOwner.get(id) : owner ?? null;
|
|
206
|
+
const tool = { toolUseId: id, name: this.toolName.get(id) ?? 'tool', phase: 'completed', output: textOf(block.content).slice(0, 16_384), ...(block.is_error === true ? { isError: true } : {}) };
|
|
207
|
+
events.push(issuer === null ? { kind: 'turn.tool', ...tool } : { kind: 'subagent.output', subagentId: issuer, channel: 'tool', tool });
|
|
208
|
+
}
|
|
209
|
+
return events;
|
|
210
|
+
}
|
|
211
|
+
result(frame) {
|
|
212
|
+
const origin = frame.origin?.kind;
|
|
213
|
+
const text = str(frame.result) ?? '';
|
|
214
|
+
if (!this.turnOpen) {
|
|
215
|
+
// An orphan notification on resume (num_turns 0, empty) carries nothing.
|
|
216
|
+
if (origin === 'task-notification')
|
|
217
|
+
return [];
|
|
218
|
+
return this.unknown('result:without-turn');
|
|
219
|
+
}
|
|
220
|
+
this.turnOpen = false;
|
|
221
|
+
const interrupted = this.interruptRequested;
|
|
222
|
+
this.interruptRequested = false;
|
|
223
|
+
const failed = frame.is_error === true || (str(frame.subtype) !== undefined && frame.subtype !== 'success');
|
|
224
|
+
const usage = frame.usage;
|
|
225
|
+
const modelUsage = frame.modelUsage;
|
|
226
|
+
return [{
|
|
227
|
+
kind: 'turn.completed',
|
|
228
|
+
status: interrupted ? 'stopped' : failed ? 'failed' : 'completed',
|
|
229
|
+
text,
|
|
230
|
+
...(failed && !interrupted ? { error: text || str(frame.subtype) || 'Provider reported an error' } : {}),
|
|
231
|
+
usage: {
|
|
232
|
+
costUsd: num(frame.total_cost_usd) ?? null,
|
|
233
|
+
inputTokens: num(usage?.input_tokens) ?? null,
|
|
234
|
+
outputTokens: num(usage?.output_tokens) ?? null,
|
|
235
|
+
cacheReadTokens: num(usage?.cache_read_input_tokens) ?? null,
|
|
236
|
+
cacheWriteTokens: num(usage?.cache_creation_input_tokens) ?? null,
|
|
237
|
+
model: modelUsage ? Object.keys(modelUsage)[0] ?? null : null,
|
|
238
|
+
},
|
|
239
|
+
}];
|
|
240
|
+
}
|
|
241
|
+
unknown(code) {
|
|
242
|
+
if (this.reportedUnknown.has(code))
|
|
243
|
+
return [];
|
|
244
|
+
this.reportedUnknown.add(code);
|
|
245
|
+
return [{ kind: 'diagnostic', level: 'info', code: 'provider.unknown', message: `Unrecognised Claude frame (${code})` }];
|
|
246
|
+
}
|
|
247
|
+
}
|