@pivanov/claude-wire 0.0.3 → 0.0.4
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/README.md +4 -2
- package/dist/async.d.ts +10 -0
- package/dist/async.js +27 -0
- package/dist/constants.d.ts +2 -0
- package/dist/constants.js +7 -0
- package/dist/cost.d.ts +2 -0
- package/dist/cost.js +5 -2
- package/dist/errors.d.ts +2 -2
- package/dist/errors.js +14 -8
- package/dist/index.d.ts +5 -4
- package/dist/index.js +3 -3
- package/dist/parser/translator.js +6 -1
- package/dist/pipeline.d.ts +14 -4
- package/dist/pipeline.js +38 -18
- package/dist/process.d.ts +15 -3
- package/dist/process.js +86 -25
- package/dist/reader.d.ts +3 -0
- package/dist/reader.js +32 -30
- package/dist/runtime.d.ts +5 -4
- package/dist/runtime.js +8 -5
- package/dist/session.d.ts +30 -2
- package/dist/session.js +129 -69
- package/dist/stream.js +60 -25
- package/dist/tools/handler.d.ts +1 -0
- package/dist/tools/registry.d.ts +4 -2
- package/dist/tools/registry.js +7 -4
- package/dist/types/options.d.ts +42 -5
- package/dist/types/protocol.d.ts +1 -5
- package/dist/validation.d.ts +10 -0
- package/dist/validation.js +23 -0
- package/dist/warnings.d.ts +2 -0
- package/dist/warnings.js +24 -0
- package/dist/writer.d.ts +10 -1
- package/dist/writer.js +14 -8
- package/package.json +1 -1
package/dist/stream.js
CHANGED
|
@@ -1,68 +1,99 @@
|
|
|
1
|
+
import { withTimeout } from "./async.js";
|
|
2
|
+
import { TIMEOUTS } from "./constants.js";
|
|
1
3
|
import { createCostTracker } from "./cost.js";
|
|
2
|
-
import { AbortError, ClaudeError, ProcessError } from "./errors.js";
|
|
4
|
+
import { AbortError, ClaudeError, ProcessError, processExitedEarly } from "./errors.js";
|
|
3
5
|
import { createTranslator } from "./parser/translator.js";
|
|
4
|
-
import { buildResult, extractText } from "./pipeline.js";
|
|
5
|
-
import {
|
|
6
|
-
import { drainStderr, readNdjsonEvents } from "./reader.js";
|
|
6
|
+
import { applyTurnComplete, buildResult, extractText, startPipeline } from "./pipeline.js";
|
|
7
|
+
import { readNdjsonEvents } from "./reader.js";
|
|
7
8
|
import { createToolHandler } from "./tools/handler.js";
|
|
9
|
+
// Enforced exclusivity between iterating the stream and consuming via
|
|
10
|
+
// text()/cost()/result(). Sharing the base message keeps the two throw
|
|
11
|
+
// sites from drifting apart over time.
|
|
12
|
+
const MIX_ITER_CONSUME = "Cannot mix for-await iteration with text()/cost()/result() on the same stream -- use one or the other.";
|
|
8
13
|
export const createStream = (prompt, options = {}) => {
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
14
|
+
// Abort check happens inside `ensureSpawned` -- at factory time we only
|
|
15
|
+
// capture config. A pre-aborted signal surfaces on the first access
|
|
16
|
+
// (iterate / text / cost / result), which is when spawn would happen.
|
|
12
17
|
const translator = createTranslator();
|
|
13
18
|
const toolHandler = options.tools ? createToolHandler(options.tools) : undefined;
|
|
14
19
|
const costTracker = createCostTracker({
|
|
15
20
|
maxCostUsd: options.maxCostUsd,
|
|
16
21
|
onCostUpdate: options.onCostUpdate,
|
|
22
|
+
onWarning: options.onWarning,
|
|
17
23
|
});
|
|
18
24
|
let proc;
|
|
19
25
|
let stderr;
|
|
26
|
+
let stdoutReader;
|
|
20
27
|
let cachedGenerator;
|
|
21
28
|
const ensureSpawned = () => {
|
|
22
29
|
if (!proc) {
|
|
23
30
|
if (options.signal?.aborted) {
|
|
24
31
|
throw new AbortError();
|
|
25
32
|
}
|
|
26
|
-
|
|
27
|
-
|
|
33
|
+
// Shared boot: spawnClaude → getReader → drainStderr in one call.
|
|
34
|
+
// Matches session.ts so future refactors can't let the two drift.
|
|
35
|
+
const pipeline = startPipeline({ prompt, ...options });
|
|
36
|
+
proc = pipeline.proc;
|
|
37
|
+
stdoutReader = pipeline.reader;
|
|
38
|
+
stderr = pipeline.stderr;
|
|
28
39
|
}
|
|
29
40
|
return proc;
|
|
30
41
|
};
|
|
31
42
|
const generate = async function* () {
|
|
32
43
|
const p = ensureSpawned();
|
|
33
|
-
|
|
44
|
+
// ensureSpawned always populates stdoutReader alongside proc. Typed
|
|
45
|
+
// assertion so consumers can treat it as non-null below.
|
|
46
|
+
const currentReader = stdoutReader;
|
|
34
47
|
let turnComplete = false;
|
|
35
48
|
try {
|
|
36
49
|
for await (const event of readNdjsonEvents({
|
|
37
|
-
reader:
|
|
50
|
+
reader: currentReader,
|
|
38
51
|
translator,
|
|
39
52
|
toolHandler,
|
|
40
53
|
proc: p,
|
|
41
54
|
signal: options.signal,
|
|
55
|
+
onWarning: options.onWarning,
|
|
42
56
|
})) {
|
|
43
57
|
if (event.type === "turn_complete") {
|
|
44
|
-
|
|
45
|
-
costTracker.checkBudget();
|
|
58
|
+
applyTurnComplete(event, costTracker);
|
|
46
59
|
turnComplete = true;
|
|
47
60
|
}
|
|
48
61
|
yield event;
|
|
49
62
|
}
|
|
50
63
|
if (!turnComplete) {
|
|
51
|
-
|
|
64
|
+
// Don't wait forever on p.exited -- a stuck child that never closes
|
|
65
|
+
// stdout would hang the generator. Cap at gracefulExitMs, then
|
|
66
|
+
// force-kill so cleanup() isn't left waiting too.
|
|
67
|
+
const exitCode = await withTimeout(p.exited, TIMEOUTS.gracefulExitMs);
|
|
68
|
+
if (exitCode === undefined) {
|
|
69
|
+
p.kill();
|
|
70
|
+
}
|
|
71
|
+
// Give stderr a brief chance to drain so the thrown error carries
|
|
72
|
+
// the CLI's actual complaint instead of an empty string. Uniform
|
|
73
|
+
// across all three branches below so users never get "no context".
|
|
74
|
+
if (stderr) {
|
|
75
|
+
await withTimeout(stderr.done, TIMEOUTS.stderrDrainGraceMs);
|
|
76
|
+
}
|
|
77
|
+
const stderrText = stderr ? stderr.text() : "";
|
|
78
|
+
if (exitCode === undefined) {
|
|
79
|
+
throw processExitedEarly(stderrText);
|
|
80
|
+
}
|
|
52
81
|
if (exitCode !== 0) {
|
|
53
|
-
if (stderr) {
|
|
54
|
-
await stderr.done;
|
|
55
|
-
}
|
|
56
|
-
const stderrText = stderr ? stderr.chunks.join("").trim() : "";
|
|
57
82
|
const exitMsg = stderrText || `Claude process exited with code ${exitCode}`;
|
|
58
83
|
throw new ProcessError(exitMsg, exitCode);
|
|
59
84
|
}
|
|
60
|
-
throw
|
|
85
|
+
throw processExitedEarly(stderrText);
|
|
61
86
|
}
|
|
62
87
|
}
|
|
63
88
|
finally {
|
|
64
|
-
|
|
89
|
+
currentReader.releaseLock();
|
|
65
90
|
p.kill();
|
|
91
|
+
// Let stderr catch up so any trailing lines aren't silently dropped --
|
|
92
|
+
// session's error path does the same via withTimeout. Capped so a
|
|
93
|
+
// stuck drain can't hold up consumer cleanup.
|
|
94
|
+
if (stderr) {
|
|
95
|
+
await withTimeout(stderr.done, TIMEOUTS.stderrDrainGraceMs);
|
|
96
|
+
}
|
|
66
97
|
}
|
|
67
98
|
};
|
|
68
99
|
const bufferedEvents = [];
|
|
@@ -70,7 +101,7 @@ export const createStream = (prompt, options = {}) => {
|
|
|
70
101
|
const ensureConsumed = () => {
|
|
71
102
|
if (!consumePromise) {
|
|
72
103
|
if (cachedGenerator) {
|
|
73
|
-
throw new ClaudeError(
|
|
104
|
+
throw new ClaudeError(MIX_ITER_CONSUME);
|
|
74
105
|
}
|
|
75
106
|
const gen = generate();
|
|
76
107
|
cachedGenerator = gen;
|
|
@@ -96,9 +127,13 @@ export const createStream = (prompt, options = {}) => {
|
|
|
96
127
|
return buildResult(bufferedEvents, costTracker, sessionId);
|
|
97
128
|
};
|
|
98
129
|
const cleanup = () => {
|
|
99
|
-
//
|
|
100
|
-
//
|
|
101
|
-
//
|
|
130
|
+
// One-shot kill: streams are single-turn, so unlike session.gracefulKill
|
|
131
|
+
// there's no second ask() to worry about leaving the child stranded for.
|
|
132
|
+
// SIGTERM is sufficient -- a stuck child would be the CLI's bug, and we
|
|
133
|
+
// wouldn't gain anything by blocking cleanup() on a SIGKILL escalation.
|
|
134
|
+
// Always kill if a proc was ever spawned -- the generator's finally may
|
|
135
|
+
// not have run yet (e.g., iterator created but never ticked). Redundant
|
|
136
|
+
// kill on an already-exited process is a harmless ESRCH.
|
|
102
137
|
if (proc) {
|
|
103
138
|
proc.kill();
|
|
104
139
|
}
|
|
@@ -106,7 +141,7 @@ export const createStream = (prompt, options = {}) => {
|
|
|
106
141
|
return {
|
|
107
142
|
[Symbol.asyncIterator]: () => {
|
|
108
143
|
if (consumePromise) {
|
|
109
|
-
throw new ClaudeError(
|
|
144
|
+
throw new ClaudeError(MIX_ITER_CONSUME);
|
|
110
145
|
}
|
|
111
146
|
cachedGenerator ??= generate();
|
|
112
147
|
return cachedGenerator;
|
package/dist/tools/handler.d.ts
CHANGED
|
@@ -2,6 +2,7 @@ import type { TToolUseEvent } from "../types/events.js";
|
|
|
2
2
|
import type { IToolHandler } from "../types/options.js";
|
|
3
3
|
export type TToolDecision = "approve" | "deny" | {
|
|
4
4
|
result: string;
|
|
5
|
+
isError?: boolean;
|
|
5
6
|
};
|
|
6
7
|
export interface IToolHandlerInstance {
|
|
7
8
|
decide: (tool: TToolUseEvent) => Promise<TToolDecision>;
|
package/dist/tools/registry.d.ts
CHANGED
|
@@ -1,2 +1,4 @@
|
|
|
1
|
-
export declare const
|
|
2
|
-
export
|
|
1
|
+
export declare const BUILT_IN_TOOL_NAMES: readonly ["Read", "Write", "Edit", "Bash", "Glob", "Grep", "Agent", "NotebookEdit", "WebFetch", "WebSearch", "TaskCreate", "TaskUpdate", "TaskGet", "TaskList", "TaskStop", "TaskOutput", "ToolSearch", "Monitor", "EnterPlanMode", "ExitPlanMode", "SendMessage", "LSP", "AskUserQuestion", "Skill", "CronCreate", "CronDelete", "CronList", "RemoteTrigger", "TeamCreate", "TeamDelete", "EnterWorktree", "ExitWorktree", "ScheduleWakeup"];
|
|
2
|
+
export type TBuiltInToolName = (typeof BUILT_IN_TOOL_NAMES)[number];
|
|
3
|
+
export declare const BUILT_IN_TOOLS: ReadonlySet<TBuiltInToolName>;
|
|
4
|
+
export declare const isBuiltInTool: (name: string) => name is TBuiltInToolName;
|
package/dist/tools/registry.js
CHANGED
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
// Best-effort snapshot of known Claude Code tools. May not be exhaustive.
|
|
2
2
|
// For the authoritative list, check session_meta.tools from a live session.
|
|
3
|
-
|
|
3
|
+
//
|
|
4
|
+
// Declared as a literal tuple so `TBuiltInToolName` is the exact union of
|
|
5
|
+
// known names -- lets callers narrow `allowedTools` / `disallowedTools`
|
|
6
|
+
// arrays at compile time instead of accepting any string[].
|
|
7
|
+
export const BUILT_IN_TOOL_NAMES = [
|
|
4
8
|
"Read",
|
|
5
9
|
"Write",
|
|
6
10
|
"Edit",
|
|
@@ -11,8 +15,6 @@ export const BUILT_IN_TOOLS = new Set([
|
|
|
11
15
|
"NotebookEdit",
|
|
12
16
|
"WebFetch",
|
|
13
17
|
"WebSearch",
|
|
14
|
-
"TodoRead",
|
|
15
|
-
"TodoWrite",
|
|
16
18
|
"TaskCreate",
|
|
17
19
|
"TaskUpdate",
|
|
18
20
|
"TaskGet",
|
|
@@ -36,7 +38,8 @@ export const BUILT_IN_TOOLS = new Set([
|
|
|
36
38
|
"EnterWorktree",
|
|
37
39
|
"ExitWorktree",
|
|
38
40
|
"ScheduleWakeup",
|
|
39
|
-
]
|
|
41
|
+
];
|
|
42
|
+
export const BUILT_IN_TOOLS = new Set(BUILT_IN_TOOL_NAMES);
|
|
40
43
|
export const isBuiltInTool = (name) => {
|
|
41
44
|
return BUILT_IN_TOOLS.has(name);
|
|
42
45
|
};
|
package/dist/types/options.d.ts
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import type { TToolDecision } from "../tools/handler.js";
|
|
2
|
+
import type { TBuiltInToolName } from "../tools/registry.js";
|
|
2
3
|
import type { TToolUseEvent } from "./events.js";
|
|
3
4
|
import type { TCostSnapshot } from "./results.js";
|
|
5
|
+
export type TToolName = TBuiltInToolName | (string & {});
|
|
4
6
|
export interface IToolHandler {
|
|
5
|
-
allowed?:
|
|
6
|
-
blocked?:
|
|
7
|
+
allowed?: TToolName[];
|
|
8
|
+
blocked?: TToolName[];
|
|
7
9
|
onToolUse?: (tool: TToolUseEvent) => Promise<TToolDecision>;
|
|
8
10
|
onError?: (error: unknown, tool: TToolUseEvent) => TToolDecision | Promise<TToolDecision>;
|
|
9
11
|
}
|
|
@@ -12,8 +14,8 @@ export interface IClaudeOptions {
|
|
|
12
14
|
model?: "opus" | "sonnet" | "haiku" | (string & {});
|
|
13
15
|
systemPrompt?: string;
|
|
14
16
|
appendSystemPrompt?: string;
|
|
15
|
-
allowedTools?:
|
|
16
|
-
disallowedTools?:
|
|
17
|
+
allowedTools?: TToolName[];
|
|
18
|
+
disallowedTools?: TToolName[];
|
|
17
19
|
tools?: IToolHandler;
|
|
18
20
|
/**
|
|
19
21
|
* SDK-side budget limit, evaluated after each turn. Throws `BudgetExceededError`
|
|
@@ -45,8 +47,43 @@ export interface IClaudeOptions {
|
|
|
45
47
|
forkSession?: boolean;
|
|
46
48
|
noSessionPersistence?: boolean;
|
|
47
49
|
sessionId?: string;
|
|
48
|
-
settingSources?: string;
|
|
50
|
+
settingSources?: "project" | "user" | "local" | "all" | "" | (string & {});
|
|
49
51
|
disableSlashCommands?: boolean;
|
|
52
|
+
/**
|
|
53
|
+
* Called for every library-emitted warning (user-callback threw, malformed
|
|
54
|
+
* tool decision, etc.). Set this to route warnings to your telemetry or
|
|
55
|
+
* silence them with `() => {}`. When omitted, warnings go to `console.warn`
|
|
56
|
+
* prefixed with `[claude-wire]`.
|
|
57
|
+
*/
|
|
58
|
+
onWarning?: (message: string, cause?: unknown) => void;
|
|
50
59
|
}
|
|
51
60
|
export interface ISessionOptions extends IClaudeOptions {
|
|
61
|
+
/**
|
|
62
|
+
* Fires each time a transient failure triggers a respawn inside a single
|
|
63
|
+
* `ask()`. `attempt` is 1-indexed. The error is the one that caused the
|
|
64
|
+
* retry (e.g. `ProcessError` with a SIGKILL exit code). Use this to
|
|
65
|
+
* surface retry activity in UI/telemetry; the SDK still handles the retry.
|
|
66
|
+
*
|
|
67
|
+
* Can also be passed per-ask via `session.ask(prompt, { onRetry })` for
|
|
68
|
+
* request-scoped correlation. Both fire if both are set.
|
|
69
|
+
*/
|
|
70
|
+
onRetry?: (attempt: number, error: unknown) => void;
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Per-ask options passed to `session.ask(prompt, options?)`. Override or
|
|
74
|
+
* supplement session-level callbacks for a single call -- useful for
|
|
75
|
+
* request-scoped logging/correlation in daemon-style consumers.
|
|
76
|
+
*/
|
|
77
|
+
export interface IAskOptions {
|
|
78
|
+
/**
|
|
79
|
+
* Per-ask retry observer. Fires alongside the session-level `onRetry` when
|
|
80
|
+
* both are set, so callers can attach request-scoped context (request id,
|
|
81
|
+
* trace span, user id) without reaching outside the callback.
|
|
82
|
+
*/
|
|
83
|
+
onRetry?: (attempt: number, error: unknown) => void;
|
|
84
|
+
/**
|
|
85
|
+
* Per-ask abort signal. Aborts this ask only (the session stays alive).
|
|
86
|
+
* Composes with the session-level `signal` -- either firing aborts the ask.
|
|
87
|
+
*/
|
|
88
|
+
signal?: AbortSignal;
|
|
52
89
|
}
|
package/dist/types/protocol.d.ts
CHANGED
|
@@ -3,7 +3,7 @@ export type TModelUsageEntry = {
|
|
|
3
3
|
outputTokens: number;
|
|
4
4
|
cacheReadInputTokens?: number;
|
|
5
5
|
cacheCreationInputTokens?: number;
|
|
6
|
-
contextWindow
|
|
6
|
+
contextWindow?: number;
|
|
7
7
|
};
|
|
8
8
|
export type TClaudeContentType = "text" | "thinking" | "tool_use" | "tool_result" | (string & {});
|
|
9
9
|
export type TClaudeContent = {
|
|
@@ -32,11 +32,7 @@ export type TClaudeEvent = {
|
|
|
32
32
|
model?: string;
|
|
33
33
|
tools?: string[];
|
|
34
34
|
duration_ms?: number;
|
|
35
|
-
duration_api_ms?: number;
|
|
36
|
-
cost_usd?: number;
|
|
37
35
|
total_cost_usd?: number;
|
|
38
36
|
is_error?: boolean;
|
|
39
|
-
num_turns?: number;
|
|
40
37
|
modelUsage?: Record<string, TModelUsageEntry>;
|
|
41
|
-
usage?: unknown;
|
|
42
38
|
};
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Rejects non-finite values and negatives. Zero is intentionally allowed
|
|
3
|
+
* so callers can express "disallow any spend" (useful in tests).
|
|
4
|
+
*/
|
|
5
|
+
export declare const assertPositiveNumber: (value: number | undefined, name: string) => void;
|
|
6
|
+
/**
|
|
7
|
+
* Rejects empty strings. Used on writer payloads where an empty value
|
|
8
|
+
* would produce a malformed JSON line on the CLI's stdin.
|
|
9
|
+
*/
|
|
10
|
+
export declare const requireNonEmpty: (value: string, name: string) => void;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { ClaudeError } from "./errors.js";
|
|
2
|
+
// Boundary validators -- throw ClaudeError with a stable message shape so
|
|
3
|
+
// callers (SDK tests, consumer apps) can pattern-match on field name.
|
|
4
|
+
// Both helpers are cheap; performance-sensitive paths should still prefer
|
|
5
|
+
// type-level guards, but runtime checks catch undeclared-undefined drift.
|
|
6
|
+
/**
|
|
7
|
+
* Rejects non-finite values and negatives. Zero is intentionally allowed
|
|
8
|
+
* so callers can express "disallow any spend" (useful in tests).
|
|
9
|
+
*/
|
|
10
|
+
export const assertPositiveNumber = (value, name) => {
|
|
11
|
+
if (value !== undefined && (!Number.isFinite(value) || value < 0)) {
|
|
12
|
+
throw new ClaudeError(`${name} must be a finite non-negative number`);
|
|
13
|
+
}
|
|
14
|
+
};
|
|
15
|
+
/**
|
|
16
|
+
* Rejects empty strings. Used on writer payloads where an empty value
|
|
17
|
+
* would produce a malformed JSON line on the CLI's stdin.
|
|
18
|
+
*/
|
|
19
|
+
export const requireNonEmpty = (value, name) => {
|
|
20
|
+
if (!value) {
|
|
21
|
+
throw new ClaudeError(`${name} must be a non-empty string`);
|
|
22
|
+
}
|
|
23
|
+
};
|
package/dist/warnings.js
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
// One-line library-warning emitter. Consumers set `onWarning` on
|
|
2
|
+
// IClaudeOptions to route warnings anywhere; when unset we fall back to
|
|
3
|
+
// `console.warn` so behavior is unchanged for casual users.
|
|
4
|
+
const DEFAULT = (message, cause) => {
|
|
5
|
+
if (cause === undefined) {
|
|
6
|
+
console.warn(`[claude-wire] ${message}`);
|
|
7
|
+
}
|
|
8
|
+
else {
|
|
9
|
+
console.warn(`[claude-wire] ${message}`, cause);
|
|
10
|
+
}
|
|
11
|
+
};
|
|
12
|
+
export const createWarn = (onWarning) => {
|
|
13
|
+
return onWarning
|
|
14
|
+
? (message, cause) => {
|
|
15
|
+
try {
|
|
16
|
+
onWarning(message, cause);
|
|
17
|
+
}
|
|
18
|
+
catch {
|
|
19
|
+
// A user hook that itself throws shouldn't take down the stream.
|
|
20
|
+
DEFAULT(message, cause);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
: DEFAULT;
|
|
24
|
+
};
|
package/dist/writer.d.ts
CHANGED
|
@@ -2,6 +2,15 @@ export declare const writer: {
|
|
|
2
2
|
user: (content: string) => string;
|
|
3
3
|
approve: (toolUseId: string) => string;
|
|
4
4
|
deny: (toolUseId: string) => string;
|
|
5
|
-
|
|
5
|
+
/**
|
|
6
|
+
* Send a tool result in response to a `tool_use` event. Pass
|
|
7
|
+
* `{ isError: true }` to mark the result as a tool-side error -- the model
|
|
8
|
+
* will see it as an error and can react (retry, apologize, pick another
|
|
9
|
+
* tool) rather than treating it as success. The protocol supports the
|
|
10
|
+
* flag natively; without it, results are assumed successful.
|
|
11
|
+
*/
|
|
12
|
+
toolResult: (toolUseId: string, content: string, options?: {
|
|
13
|
+
isError?: boolean;
|
|
14
|
+
}) => string;
|
|
6
15
|
abort: () => string;
|
|
7
16
|
};
|
package/dist/writer.js
CHANGED
|
@@ -1,10 +1,5 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { requireNonEmpty } from "./validation.js";
|
|
2
2
|
const ABORT_LINE = `${JSON.stringify({ type: "abort" })}\n`;
|
|
3
|
-
const requireNonEmpty = (value, name) => {
|
|
4
|
-
if (!value) {
|
|
5
|
-
throw new ClaudeError(`${name} must be a non-empty string`);
|
|
6
|
-
}
|
|
7
|
-
};
|
|
8
3
|
export const writer = {
|
|
9
4
|
user: (content) => {
|
|
10
5
|
requireNonEmpty(content, "content");
|
|
@@ -18,9 +13,20 @@ export const writer = {
|
|
|
18
13
|
requireNonEmpty(toolUseId, "toolUseId");
|
|
19
14
|
return `${JSON.stringify({ type: "deny", tool_use_id: toolUseId })}\n`;
|
|
20
15
|
},
|
|
21
|
-
|
|
16
|
+
/**
|
|
17
|
+
* Send a tool result in response to a `tool_use` event. Pass
|
|
18
|
+
* `{ isError: true }` to mark the result as a tool-side error -- the model
|
|
19
|
+
* will see it as an error and can react (retry, apologize, pick another
|
|
20
|
+
* tool) rather than treating it as success. The protocol supports the
|
|
21
|
+
* flag natively; without it, results are assumed successful.
|
|
22
|
+
*/
|
|
23
|
+
toolResult: (toolUseId, content, options) => {
|
|
22
24
|
requireNonEmpty(toolUseId, "toolUseId");
|
|
23
|
-
|
|
25
|
+
const payload = { type: "tool_result", tool_use_id: toolUseId, content };
|
|
26
|
+
if (options?.isError) {
|
|
27
|
+
payload.is_error = true;
|
|
28
|
+
}
|
|
29
|
+
return `${JSON.stringify(payload)}\n`;
|
|
24
30
|
},
|
|
25
31
|
abort: () => ABORT_LINE,
|
|
26
32
|
};
|