@naswerks/periscope 1.0.0 → 1.0.1
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/CHANGELOG.md +6 -0
- package/README.md +2 -2
- package/contracts/wire-vectors/bulk_delivered.full.json +21 -0
- package/contracts/wire-vectors/bulk_delivered.minimal.json +21 -0
- package/contracts/wire-vectors/bulk_delivered.without-stat-pair.json +22 -0
- package/contracts/wire-vectors/bulk_failed.declared-reason.json +22 -0
- package/contracts/wire-vectors/bulk_failed.unknown-reason.json +23 -0
- package/contracts/wire-vectors/bulk_request.full.json +21 -0
- package/contracts/wire-vectors/host_configure.set-and-remove.json +28 -0
- package/contracts/wire-vectors/host_configure_result.applied.json +34 -0
- package/contracts/wire-vectors/host_configure_result.refused.json +33 -0
- package/contracts/wire-vectors/link_ack.cursors.json +25 -0
- package/contracts/wire-vectors/link_ack.empty.json +16 -0
- package/contracts/wire-vectors/link_bye.cause.json +16 -0
- package/contracts/wire-vectors/link_hello.empty.json +33 -0
- package/contracts/wire-vectors/link_hello.full.json +44 -0
- package/contracts/wire-vectors/link_ping.nonce.json +16 -0
- package/contracts/wire-vectors/link_ping.unknown-payload-key.json +18 -0
- package/contracts/wire-vectors/link_pong.nonce.json +16 -0
- package/contracts/wire-vectors/link_welcome.empty.json +18 -0
- package/contracts/wire-vectors/link_welcome.full.json +25 -0
- package/contracts/wire-vectors/refused.empty.json +10 -0
- package/contracts/wire-vectors/refused.encode.empty-at.json +18 -0
- package/contracts/wire-vectors/refused.encode.empty-session-id.json +18 -0
- package/contracts/wire-vectors/refused.encode.missing-member.json +22 -0
- package/contracts/wire-vectors/refused.encode.seq-zero.json +18 -0
- package/contracts/wire-vectors/refused.encode.undeclared-configure-refusal.json +34 -0
- package/contracts/wire-vectors/refused.encode.undeclared-wire-refusal.json +27 -0
- package/contracts/wire-vectors/refused.missing-member.json +10 -0
- package/contracts/wire-vectors/refused.not-json.json +10 -0
- package/contracts/wire-vectors/refused.over-limit.json +10 -0
- package/contracts/wire-vectors/refused.seq-zero.json +10 -0
- package/contracts/wire-vectors/refused.unknown-frame.json +10 -0
- package/contracts/wire-vectors/refused.unknown-kind.json +10 -0
- package/contracts/wire-vectors/repository_list.root.json +19 -0
- package/contracts/wire-vectors/repository_list.subdirectory.json +19 -0
- package/contracts/wire-vectors/repository_list_result.entries.json +34 -0
- package/contracts/wire-vectors/repository_list_result.refused.json +24 -0
- package/contracts/wire-vectors/repository_read.head.json +20 -0
- package/contracts/wire-vectors/repository_read_result.refused.json +25 -0
- package/contracts/wire-vectors/repository_read_result.truncated.json +22 -0
- package/contracts/wire-vectors/session_cancel.bare.json +17 -0
- package/contracts/wire-vectors/session_configure.full.json +22 -0
- package/contracts/wire-vectors/session_configure.minimal.json +20 -0
- package/contracts/wire-vectors/session_delta.agent-message.json +27 -0
- package/contracts/wire-vectors/session_list.request.json +18 -0
- package/contracts/wire-vectors/session_list_result.empty.json +21 -0
- package/contracts/wire-vectors/session_list_result.full.json +36 -0
- package/contracts/wire-vectors/session_new.full.json +72 -0
- package/contracts/wire-vectors/session_new.minimal.json +22 -0
- package/contracts/wire-vectors/session_new.request-all-null.json +41 -0
- package/contracts/wire-vectors/session_prompt.empty.json +18 -0
- package/contracts/wire-vectors/session_prompt.multibyte.json +18 -0
- package/contracts/wire-vectors/session_prompt.text.json +18 -0
- package/contracts/wire-vectors/session_prompt.unknown-frame-key.json +20 -0
- package/contracts/wire-vectors/session_prompt.unknown-payload-key.json +20 -0
- package/contracts/wire-vectors/session_update.agent-message.json +26 -0
- package/contracts/wire-vectors/session_update.state-transition-with-activity.json +44 -0
- package/contracts/wire-vectors/session_update.state-transition.json +41 -0
- package/contracts/wire-vectors/session_update.wire-refusal.json +26 -0
- package/contracts/wire-vectors/transcript_failed.declared-reason.json +22 -0
- package/contracts/wire-vectors/transcript_list.from-start.json +19 -0
- package/contracts/wire-vectors/transcript_list.paged.json +19 -0
- package/contracts/wire-vectors/transcript_list_result.entry-without-cwd.json +30 -0
- package/contracts/wire-vectors/transcript_list_result.full.json +29 -0
- package/contracts/wire-vectors/transcript_list_result.last-page.json +29 -0
- package/contracts/wire-vectors/transcript_tail.any-user-entry.json +22 -0
- package/contracts/wire-vectors/transcript_tail.with-needle.json +22 -0
- package/contracts/wire-vectors/transcript_tail_result.absent.json +23 -0
- package/contracts/wire-vectors/transcript_tail_result.found.json +23 -0
- package/contracts/wire-vectors/workspace_list.from-start.json +19 -0
- package/contracts/wire-vectors/workspace_list_result.page.json +48 -0
- package/contracts/wire-vectors/workspace_list_result.refused.json +26 -0
- package/contracts/wire-vectors/workspace_release.by-path-delete-branch.json +22 -0
- package/contracts/wire-vectors/workspace_release.request.json +22 -0
- package/contracts/wire-vectors/workspace_release_bulk.two-entries.json +32 -0
- package/contracts/wire-vectors/workspace_release_bulk_result.mixed.json +37 -0
- package/contracts/wire-vectors/workspace_release_result.partial.json +26 -0
- package/contracts/wire-vectors/workspace_release_result.refused.json +26 -0
- package/contracts/wire-vectors/workspace_release_result.released.json +23 -0
- package/package.json +6 -1
- package/src/bin/command.ts +198 -0
- package/src/bin/config.ts +104 -0
- package/src/bin/login.ts +149 -0
- package/src/bin/main.ts +204 -0
- package/src/bin/pair.ts +246 -0
- package/src/bin/periscope.ts +3 -0
- package/src/bin/posture.ts +232 -0
- package/src/bin/reconfigure.ts +266 -0
- package/src/bin/serve.ts +615 -0
- package/src/bin/status.ts +61 -0
- package/src/bin/workspaces.ts +280 -0
- package/src/control/backoff.ts +40 -0
- package/src/control/codec.ts +557 -0
- package/src/control/credential.ts +33 -0
- package/src/control/frames.ts +1586 -0
- package/src/control/link-state.ts +67 -0
- package/src/control/link.ts +894 -0
- package/src/control/queue.ts +248 -0
- package/src/control/seq.ts +76 -0
- package/src/control/stream-routing.ts +216 -0
- package/src/control/stream.ts +149 -0
- package/src/core/async-queue.ts +63 -0
- package/src/core/failure.ts +61 -0
- package/src/core/index.ts +22 -0
- package/src/core/keyed-turns.ts +47 -0
- package/src/core/paths.ts +84 -0
- package/src/core/refusal.ts +430 -0
- package/src/core/result.ts +27 -0
- package/src/core/time.ts +34 -0
- package/src/core/vocab.ts +44 -0
- package/src/core/workspace-id.ts +11 -0
- package/src/gate/command.ts +617 -0
- package/src/gate/decision.ts +173 -0
- package/src/gate/escalate.ts +164 -0
- package/src/gate/gate.ts +635 -0
- package/src/gate/index.ts +53 -0
- package/src/gate/jail.ts +210 -0
- package/src/gate/local.ts +115 -0
- package/src/gate/outcome.ts +164 -0
- package/src/gate/shell.ts +295 -0
- package/src/host/agent-process.ts +694 -0
- package/src/host/bulk-post.ts +166 -0
- package/src/host/claude-transcripts.ts +404 -0
- package/src/host/config-file.ts +246 -0
- package/src/host/git-facts.ts +119 -0
- package/src/host/hooks.ts +102 -0
- package/src/host/host.ts +1901 -0
- package/src/host/index.ts +92 -0
- package/src/host/link-state-file.ts +79 -0
- package/src/host/loopback.ts +177 -0
- package/src/host/machine.ts +27 -0
- package/src/host/mcp-server.ts +66 -0
- package/src/host/package-facts.ts +25 -0
- package/src/host/paired-credential-store.ts +140 -0
- package/src/host/paths.ts +175 -0
- package/src/host/repository-read.ts +245 -0
- package/src/host/session-store.ts +133 -0
- package/src/host/sign-in.ts +352 -0
- package/src/host/telemetry.ts +105 -0
- package/src/host/token-cache.ts +214 -0
- package/src/host/transcript-fs.ts +110 -0
- package/src/host/wire-request.ts +285 -0
- package/src/host/workspace-fs.ts +90 -0
- package/src/host/workspace-trust.ts +92 -0
- package/src/identity/authorize.ts +127 -0
- package/src/identity/config.ts +227 -0
- package/src/identity/credential.ts +176 -0
- package/src/identity/device-code.ts +217 -0
- package/src/identity/index.ts +84 -0
- package/src/identity/mode.ts +161 -0
- package/src/identity/paired-credential.ts +89 -0
- package/src/identity/pkce.ts +113 -0
- package/src/identity/store.ts +98 -0
- package/src/identity/token.ts +203 -0
- package/src/index.ts +140 -0
- package/src/mcp/descriptor.ts +75 -0
- package/src/mcp/index.ts +23 -0
- package/src/mcp/schema.ts +214 -0
- package/src/mcp/server.ts +180 -0
- package/src/persistence/entry.ts +142 -0
- package/src/persistence/index.ts +47 -0
- package/src/persistence/key.ts +77 -0
- package/src/persistence/mirror.ts +118 -0
- package/src/persistence/receipt.ts +182 -0
- package/src/persistence/retention.ts +118 -0
- package/src/persistence/store.ts +183 -0
- package/src/persistence/transition-log.ts +176 -0
- package/src/protocol.ts +215 -0
- package/src/sessions/index.ts +27 -0
- package/src/sessions/registry.ts +392 -0
- package/src/sessions/session.ts +440 -0
- package/src/sessions/spawn-env.ts +318 -0
- package/src/state/coverage.ts +360 -0
- package/src/state/index.ts +58 -0
- package/src/state/machine.ts +359 -0
- package/src/state/model.ts +417 -0
- package/src/state/observer.ts +499 -0
- package/src/state/reporter.ts +60 -0
- package/src/state/store.ts +157 -0
- package/src/telemetry/index.ts +10 -0
- package/src/telemetry/usage.ts +117 -0
- package/src/workspace/git-worktree.ts +551 -0
- package/src/workspace/index.ts +17 -0
- package/src/workspace/plain-dir.ts +132 -0
- package/src/workspace/provider.ts +159 -0
- package/src/workspace/worktree-porcelain.ts +118 -0
|
@@ -0,0 +1,694 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The agent SDK seam: the only module in this package that starts an agent process, and the one
|
|
3
|
+
* that re-exports the SDK's types for every layer above.
|
|
4
|
+
*
|
|
5
|
+
* It is not the only file that names `@anthropic-ai/claude-agent-sdk`. Three others in
|
|
6
|
+
* `src/host/` do (the MCP server builder, the store adapter and the telemetry reader), because
|
|
7
|
+
* each bridges one SDK shape and none of them spawns anything. The rule the pin enforces is the
|
|
8
|
+
* directory, not this file; what remains unique here is `query()`.
|
|
9
|
+
*
|
|
10
|
+
* It lives in `src/host/` because calling `query()` spawns a real CLI subprocess. The boundary this
|
|
11
|
+
* directory holds is stated as "nothing outside `src/host/` touches the filesystem, spawns a
|
|
12
|
+
* process, or reads the machine", and a package import that spawns is exactly as privileged as
|
|
13
|
+
* `node:child_process` even though no lint rule would have noticed. Pinned by
|
|
14
|
+
* src/pins/sdk-confinement.test.ts.
|
|
15
|
+
*
|
|
16
|
+
* Everything above this file works in the package's own vocabulary and never imports the SDK, so
|
|
17
|
+
* "what starts a process, and with what?" has one answer. The SDK types the layer above genuinely
|
|
18
|
+
* needs are re-exported here rather than imported there; a type-only import would still name the
|
|
19
|
+
* specifier, and one place naming it is the whole point.
|
|
20
|
+
*
|
|
21
|
+
* `windowsHide` is not a knob this package has, and it does not need to be. Node's default is
|
|
22
|
+
* `false` (the widespread "true since Node 15" belief is wrong), and where no ancestor process owns
|
|
23
|
+
* a console there is none to inherit, so every un-hidden spawn allocates a real visible window. It
|
|
24
|
+
* is absent from the SDK's `Options`, but the SDK's own `spawnLocalProcess` passes
|
|
25
|
+
* `windowsHide: true`, so the default path is correct. The trap is `spawnClaudeCodeProcess`: a
|
|
26
|
+
* custom spawn function replaces that path entirely, and one written without `windowsHide`
|
|
27
|
+
* reintroduces a flashing window on every spawn.
|
|
28
|
+
*/
|
|
29
|
+
import { query } from '@anthropic-ai/claude-agent-sdk';
|
|
30
|
+
import type {
|
|
31
|
+
EffortLevel,
|
|
32
|
+
HookCallbackMatcher,
|
|
33
|
+
HookEvent,
|
|
34
|
+
HookInput,
|
|
35
|
+
HookJSONOutput,
|
|
36
|
+
McpSdkServerConfigWithInstance,
|
|
37
|
+
McpServerConfig,
|
|
38
|
+
Options,
|
|
39
|
+
PermissionMode,
|
|
40
|
+
Query,
|
|
41
|
+
SDKMessage,
|
|
42
|
+
SDKUserMessage,
|
|
43
|
+
SdkPluginConfig,
|
|
44
|
+
SessionStore,
|
|
45
|
+
SessionStoreFlush,
|
|
46
|
+
SettingSource,
|
|
47
|
+
SpawnOptions,
|
|
48
|
+
SpawnedProcess,
|
|
49
|
+
ThinkingConfig,
|
|
50
|
+
} from '@anthropic-ai/claude-agent-sdk';
|
|
51
|
+
|
|
52
|
+
import { AsyncQueue } from '../core/async-queue.js';
|
|
53
|
+
|
|
54
|
+
export type {
|
|
55
|
+
HookCallbackMatcher,
|
|
56
|
+
HookEvent,
|
|
57
|
+
HookInput,
|
|
58
|
+
HookJSONOutput,
|
|
59
|
+
McpSdkServerConfigWithInstance,
|
|
60
|
+
McpServerConfig,
|
|
61
|
+
SDKMessage,
|
|
62
|
+
SDKUserMessage,
|
|
63
|
+
SdkPluginConfig,
|
|
64
|
+
SessionStore,
|
|
65
|
+
SessionStoreFlush,
|
|
66
|
+
SettingSource,
|
|
67
|
+
SpawnOptions,
|
|
68
|
+
SpawnedProcess,
|
|
69
|
+
ThinkingConfig,
|
|
70
|
+
EffortLevel,
|
|
71
|
+
PermissionMode,
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
/** Exactly the shape `Options.hooks` takes, so a composer above cannot drift from it. */
|
|
75
|
+
export type HookRegistrations = Partial<Record<HookEvent, HookCallbackMatcher[]>>;
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* The message union's discriminator, as one string per shape — `type`, or `type/subtype` for the
|
|
79
|
+
* `system` family.
|
|
80
|
+
*
|
|
81
|
+
* Derived from the union rather than listed, so a message type the SDK adds shows up as a missing
|
|
82
|
+
* key wherever this is used exhaustively instead of being silently absent.
|
|
83
|
+
*/
|
|
84
|
+
type SystemSubtype = Extract<SDKMessage, { type: 'system' }>['subtype'];
|
|
85
|
+
export type MessageDiscriminator = Exclude<SDKMessage['type'], 'system'> | `system/${SystemSubtype}`;
|
|
86
|
+
|
|
87
|
+
/** The discriminator of one message, in the same form `MessageDiscriminator` names. */
|
|
88
|
+
export function discriminatorOf(message: SDKMessage): MessageDiscriminator {
|
|
89
|
+
return message.type === 'system' ? (`system/${message.subtype}` as MessageDiscriminator) : message.type;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** How the agent's process is created. Replaces the SDK's own local spawn when supplied. */
|
|
93
|
+
export type SpawnAgentProcess = (options: SpawnOptions) => SpawnedProcess;
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* What the layer above asks for. Deliberately NOT the SDK's `Options`: composing it is pure work
|
|
97
|
+
* that belongs outside this directory, and keeping the SDK's shape on this side of the seam is what
|
|
98
|
+
* lets the composition be unit-tested with no process anywhere near it.
|
|
99
|
+
*/
|
|
100
|
+
export interface AgentProcessRequest {
|
|
101
|
+
/** Absolute, always. The agent's working directory. */
|
|
102
|
+
readonly cwd: string;
|
|
103
|
+
/** The complete environment. Never merged with `process.env` — see sessions/spawn-env.ts. */
|
|
104
|
+
readonly env: Record<string, string>;
|
|
105
|
+
/**
|
|
106
|
+
* Which on-disk settings tiers the agent may load. `[]` means none.
|
|
107
|
+
*
|
|
108
|
+
* The SDK loads ALL of them when this is omitted, so it is passed explicitly on every start: a
|
|
109
|
+
* default that reads whatever files happen to be beside the working directory is not a default a
|
|
110
|
+
* host can reason about.
|
|
111
|
+
*/
|
|
112
|
+
readonly settingSources: readonly SettingSource[];
|
|
113
|
+
readonly plugins: readonly SdkPluginConfig[] | null;
|
|
114
|
+
/**
|
|
115
|
+
* The hook callbacks this session runs. Passed straight through, never composed here.
|
|
116
|
+
*
|
|
117
|
+
* Each event maps to an ARRAY of matchers and the SDK runs every entry, so observation and
|
|
118
|
+
* decision are separate registrations on the same event rather than one handler doing both. See
|
|
119
|
+
* `hooks.ts` for the composer that merges them.
|
|
120
|
+
*/
|
|
121
|
+
readonly hooks: HookRegistrations | null;
|
|
122
|
+
/** A session id to continue. Null starts a new one. */
|
|
123
|
+
readonly resume: string | null;
|
|
124
|
+
/** With `resume`, continue into a NEW session id instead of extending the old one. */
|
|
125
|
+
readonly fork: boolean;
|
|
126
|
+
/**
|
|
127
|
+
* Emit `stream_event` messages while the agent composes a turn.
|
|
128
|
+
*
|
|
129
|
+
* Nothing partial reaches the message stream without this: with it off there is no
|
|
130
|
+
* `SDKPartialAssistantMessage` at all, so text arrives only when a block completes and a turn
|
|
131
|
+
* cannot be rendered as it happens. It is also the precondition for any thinking prose.
|
|
132
|
+
*/
|
|
133
|
+
readonly includePartialMessages: boolean;
|
|
134
|
+
/**
|
|
135
|
+
* How much of the agent's reasoning is emitted. `null` leaves the SDK's own default.
|
|
136
|
+
*
|
|
137
|
+
* `{type:'adaptive'}` fires `thinking_delta` events whose prose is EMPTY;
|
|
138
|
+
* `{type:'adaptive', display:'summarized'}` streams real reasoning text — which costs tokens on
|
|
139
|
+
* the wire and puts reasoning into transcripts and mirrors, so it is asked for rather than
|
|
140
|
+
* assumed. Requires `includePartialMessages`.
|
|
141
|
+
*/
|
|
142
|
+
readonly thinking: ThinkingConfig | null;
|
|
143
|
+
/**
|
|
144
|
+
* Forward a subagent's full conversation, not only its tool calls.
|
|
145
|
+
*
|
|
146
|
+
* Off, the stream carries a subagent's `tool_use`/`tool_result` blocks and nothing else — enough
|
|
147
|
+
* to count activity, not enough to read. On, its text and thinking arrive as ordinary messages
|
|
148
|
+
* with `parent_tool_use_id` set, which is the only way a nested turn can be rendered live.
|
|
149
|
+
*/
|
|
150
|
+
readonly forwardSubagentText: boolean;
|
|
151
|
+
/**
|
|
152
|
+
* The agent process's stderr, line by line.
|
|
153
|
+
*
|
|
154
|
+
* Wired on every start because some conditions are reported here and NOWHERE else — an untrusted
|
|
155
|
+
* workspace voids its permission rules with a stderr line and no error.
|
|
156
|
+
*/
|
|
157
|
+
readonly onStderr: ((data: string) => void) | null;
|
|
158
|
+
/**
|
|
159
|
+
* In-process MCP servers this session may call, by server name.
|
|
160
|
+
*
|
|
161
|
+
* This key enlarges the set of tools the gate must decide about. It is safe for a structural
|
|
162
|
+
* reason, stated at `TOOL_SURFACE_OPTION_KEYS` below, and not for the reason the streaming keys
|
|
163
|
+
* are safe.
|
|
164
|
+
*/
|
|
165
|
+
readonly mcpServers: Readonly<Record<string, McpServerConfig>> | null;
|
|
166
|
+
/**
|
|
167
|
+
* Use ONLY the servers above, ignoring project `.mcp.json`, user settings, plugin MCP and on-disk
|
|
168
|
+
* agent frontmatter. `null` leaves the SDK's own default.
|
|
169
|
+
*
|
|
170
|
+
* The composer above defaults this on (see sessions/registry.ts). It is the workspace-MCP
|
|
171
|
+
* attack-surface switch: without it, a repository the host provisioned as a workspace can declare
|
|
172
|
+
* its own MCP servers and the agent picks them up, which means a directory's CONTENTS decide what
|
|
173
|
+
* tools exist. That is the one lane where a workspace becomes a tool-registration channel.
|
|
174
|
+
*/
|
|
175
|
+
readonly strictMcpConfig: boolean | null;
|
|
176
|
+
/**
|
|
177
|
+
* Mirror this session's transcript to a store the embedder supplies. `null` mirrors nowhere.
|
|
178
|
+
*
|
|
179
|
+
* This object receives every message the agent saw. It is a confidentiality and egress surface,
|
|
180
|
+
* and it must not be filed with the streaming keys, which only widen what this host itself
|
|
181
|
+
* observes.
|
|
182
|
+
*
|
|
183
|
+
* What makes it safe is structural, and it is the same shape as the gate's no-matcher argument:
|
|
184
|
+
* a store is a live object with methods, so it cannot be expressed in JSON and cannot cross the
|
|
185
|
+
* link. It can only be supplied by the code that composes this host, which is the embedder. No
|
|
186
|
+
* controller, and nothing arriving over the wire, can name a destination for a transcript. Pinned
|
|
187
|
+
* by pins/persistence-egress.test.ts rather than left as a sentence.
|
|
188
|
+
*
|
|
189
|
+
* The one documented incompatibility is unreachable by construction. A store may not be
|
|
190
|
+
* combined with session persistence turned off, because the mirror runs after the local write
|
|
191
|
+
* succeeds. `persistSession` is not composable through this type, so the SDK's own default stands
|
|
192
|
+
* and the combination cannot be built here.
|
|
193
|
+
*/
|
|
194
|
+
readonly sessionStore: SessionStore | null;
|
|
195
|
+
/**
|
|
196
|
+
* How eagerly mirrored entries are flushed. `null` leaves the SDK's own default of batching.
|
|
197
|
+
*
|
|
198
|
+
* Eager gives near-real-time delivery at the cost of one call per frame with no coalescing, so it
|
|
199
|
+
* is asked for rather than assumed. Ignored entirely when no store is set.
|
|
200
|
+
*/
|
|
201
|
+
readonly sessionStoreFlush: SessionStoreFlush | null;
|
|
202
|
+
/**
|
|
203
|
+
* Create the process yourself, for a VM, a container or a remote machine.
|
|
204
|
+
*
|
|
205
|
+
* Two things a custom spawn loses, and both are silent.
|
|
206
|
+
* 1. `windowsHide`. The SDK's own spawn passes it; Node's default is `false`, so a custom spawn
|
|
207
|
+
* written without it allocates a real visible window per session wherever no ancestor process
|
|
208
|
+
* owns a console. See this file's header.
|
|
209
|
+
* 2. stderr. `SpawnedProcess` carries only stdin and stdout, so a custom spawn that does not
|
|
210
|
+
* route the child's stderr somewhere loses `onStderr` entirely, and the untrusted-workspace
|
|
211
|
+
* condition is reported only there.
|
|
212
|
+
*/
|
|
213
|
+
readonly spawn: SpawnAgentProcess | null;
|
|
214
|
+
/**
|
|
215
|
+
* Which model runs this session. `null` leaves the CLI's own default.
|
|
216
|
+
*
|
|
217
|
+
* Absent, not narrowed: `model` and `systemPrompt` were never among this type's keys, so adding
|
|
218
|
+
* them filled a gap; it did not widen a security narrowing. The eight `SHADOWING_LANES` below are
|
|
219
|
+
* a deliberate narrowing and they stay closed. Two different facts, and conflating them costs a
|
|
220
|
+
* reader a whole cycle on the wrong objection.
|
|
221
|
+
*
|
|
222
|
+
* What made the gap expensive: a host that cannot state a model cannot run the same agent twice on
|
|
223
|
+
* purpose, and every caller silently got whatever the installed CLI defaulted to.
|
|
224
|
+
*/
|
|
225
|
+
readonly model: string | null;
|
|
226
|
+
/**
|
|
227
|
+
* What the agent is told at the start. `null` leaves the CLI's own preset.
|
|
228
|
+
*
|
|
229
|
+
* The one composable key that cannot be proven from the agent's own report. `AgentInitFacts`
|
|
230
|
+
* below is lifted entirely out of the SDK's `system/init` message, and that message carries no
|
|
231
|
+
* system prompt, so unlike `model`, `tools` or `mcpServers`, nothing coming back from the agent
|
|
232
|
+
* confirms this took effect. It ships unproven-by-report, and that is stated here rather than left
|
|
233
|
+
* for someone to discover while trying to write the assertion.
|
|
234
|
+
*
|
|
235
|
+
* Typed off the SDK's own option so it cannot drift from what `query()` accepts.
|
|
236
|
+
*/
|
|
237
|
+
readonly systemPrompt: AgentSystemPrompt | null;
|
|
238
|
+
/** The effort level. `null` leaves the SDK's own default. */
|
|
239
|
+
readonly effort: EffortLevel | null;
|
|
240
|
+
/**
|
|
241
|
+
* The permission mode. `null` leaves the SDK's default. Deliberately opened: see
|
|
242
|
+
* `CLI_PARITY_OPTION_KEYS` for the reason stated as data, and `SHADOWING_LANES` for what stays
|
|
243
|
+
* closed. The gate's authority is the `PreToolUse` hook; a mode never pre-answers it.
|
|
244
|
+
*/
|
|
245
|
+
readonly permissionMode: PermissionMode | null;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* What the SDK accepts as a system prompt — a string, a list of strings, or a preset descriptor.
|
|
250
|
+
*
|
|
251
|
+
* Derived from `Options` rather than restated: this package's rule is that the types win, and a
|
|
252
|
+
* hand-copied union is a second copy that can disagree with the first.
|
|
253
|
+
*/
|
|
254
|
+
export type AgentSystemPrompt = NonNullable<Options['systemPrompt']>;
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* Every key `Options` is composed from, as data.
|
|
258
|
+
*
|
|
259
|
+
* This is the permission-config pin's subject, and it is why that pin is a compile error rather
|
|
260
|
+
* than a grep. The SDK's `Options` carries nine lanes that alter permission outcomes:
|
|
261
|
+
* `permissionMode`, `settings`, `managedSettings`, `toolAliases`, `permissionPromptToolName`,
|
|
262
|
+
* `allowedTools`, `disallowedTools`, `canUseTool`, and the `permissions` block a settings object can
|
|
263
|
+
* carry. Eight of them (`SHADOWING_LANES`) are unreachable, because a caller can only supply the
|
|
264
|
+
* keys below and `startAgentProcess` composes `Options` from exactly these; `permissionMode` is the
|
|
265
|
+
* one opened by name, in `CLI_PARITY_OPTION_KEYS`. Adding a composable option breaks this
|
|
266
|
+
* declaration, and the pin fails at build time instead of when someone remembers to look.
|
|
267
|
+
*
|
|
268
|
+
* The composable set is declared as several lists, each with its own reason, because the moment two
|
|
269
|
+
* different justifications share one constant, neither can be checked:
|
|
270
|
+
*
|
|
271
|
+
* - `STREAMING_OPTION_KEYS` select what the process emits, never what it may do. A widening for
|
|
272
|
+
* visibility is not a weakening for permission.
|
|
273
|
+
* - `TOOL_SURFACE_OPTION_KEYS` change which tools exist. Read that list's note for why it is
|
|
274
|
+
* nonetheless safe.
|
|
275
|
+
* - `PERSISTENCE_OPTION_KEYS` decide where the transcript goes: the egress lane. Its safety
|
|
276
|
+
* argument is structural and is pinned rather than stated.
|
|
277
|
+
* - `AGENT_SELECTION_OPTION_KEYS` choose which agent runs and what it is told at the start.
|
|
278
|
+
* - `CLI_PARITY_OPTION_KEYS` expose what the CLI itself exposes, including the one shadowing lane
|
|
279
|
+
* that is a posture rather than a rule file.
|
|
280
|
+
*
|
|
281
|
+
* The distinction that decides whether a widening is a weakening: `model` and `systemPrompt` were
|
|
282
|
+
* absent, not narrowed. They were never among the composable keys, so opening them filled a gap.
|
|
283
|
+
* The eight lanes in `SHADOWING_LANES` are a deliberate security narrowing and stay closed, and the
|
|
284
|
+
* pin still asserts each one by name. A gap filled and a narrowing widened are different acts and
|
|
285
|
+
* this file tells them apart.
|
|
286
|
+
*
|
|
287
|
+
* `toolAliases` is the one worth naming: it redirects tool names after the model emits them, so a
|
|
288
|
+
* gate matching on `tool_name` would see the alias source while the target executed. A silent
|
|
289
|
+
* mismatch, and unreachable here by construction.
|
|
290
|
+
*/
|
|
291
|
+
export const AGENT_PROCESS_REQUEST_KEYS = {
|
|
292
|
+
cwd: true,
|
|
293
|
+
env: true,
|
|
294
|
+
settingSources: true,
|
|
295
|
+
plugins: true,
|
|
296
|
+
hooks: true,
|
|
297
|
+
resume: true,
|
|
298
|
+
fork: true,
|
|
299
|
+
onStderr: true,
|
|
300
|
+
spawn: true,
|
|
301
|
+
includePartialMessages: true,
|
|
302
|
+
thinking: true,
|
|
303
|
+
forwardSubagentText: true,
|
|
304
|
+
mcpServers: true,
|
|
305
|
+
strictMcpConfig: true,
|
|
306
|
+
sessionStore: true,
|
|
307
|
+
sessionStoreFlush: true,
|
|
308
|
+
model: true,
|
|
309
|
+
systemPrompt: true,
|
|
310
|
+
effort: true,
|
|
311
|
+
permissionMode: true,
|
|
312
|
+
} as const satisfies Record<keyof AgentProcessRequest, true>;
|
|
313
|
+
|
|
314
|
+
/**
|
|
315
|
+
* The eight closed lanes, as data: the subject both permission pins are about.
|
|
316
|
+
*
|
|
317
|
+
* The defect this guards against: the lanes were once written out twice, once per pin, and the two
|
|
318
|
+
* copies disagreed. The scan pin listed `permissionPrompt`, which does not exist in `sdk.d.ts` at
|
|
319
|
+
* all, and omitted `permissions`, which does. So the "two mechanisms, one invariant" argument (a
|
|
320
|
+
* type cannot see a module that reaches past the composer, a scan cannot see a type) was being made
|
|
321
|
+
* by two lists guarding different sets, with the real option covered by only one of them.
|
|
322
|
+
*
|
|
323
|
+
* One declaration now, derived by both, and every name checked against the shipped types.
|
|
324
|
+
*/
|
|
325
|
+
export const SHADOWING_LANES: readonly string[] = [
|
|
326
|
+
// `permissionMode` was deliberately removed from this list (CLI parity); see
|
|
327
|
+
// `CLI_PARITY_OPTION_KEYS`. It is the one lane that is a posture the operator chooses in the open,
|
|
328
|
+
// not a rule file or a pre-answer; the eight below are the latter and stay closed.
|
|
329
|
+
'settings',
|
|
330
|
+
'managedSettings',
|
|
331
|
+
'toolAliases',
|
|
332
|
+
'permissionPromptToolName',
|
|
333
|
+
'allowedTools',
|
|
334
|
+
'disallowedTools',
|
|
335
|
+
'canUseTool',
|
|
336
|
+
'permissions',
|
|
337
|
+
];
|
|
338
|
+
|
|
339
|
+
/**
|
|
340
|
+
* The keys that widened the composable set for visibility, kept as data so the reason is checkable
|
|
341
|
+
* rather than remembered.
|
|
342
|
+
*
|
|
343
|
+
* One reason covers all three: each selects what the process emits. None appears in any permission
|
|
344
|
+
* evaluation path, so none can change whether a tool runs, only how much of the run is visible.
|
|
345
|
+
* That is why widening the set here does not weaken the boundary above.
|
|
346
|
+
*
|
|
347
|
+
* The pin asserts every member is composable and is none of the eight shadowing lanes, so a later
|
|
348
|
+
* addition cannot join this list by assertion alone.
|
|
349
|
+
*/
|
|
350
|
+
export const STREAMING_OPTION_KEYS = [
|
|
351
|
+
'includePartialMessages',
|
|
352
|
+
'thinking',
|
|
353
|
+
'forwardSubagentText',
|
|
354
|
+
] as const satisfies readonly (keyof AgentProcessRequest)[];
|
|
355
|
+
|
|
356
|
+
/**
|
|
357
|
+
* The keys that change which tools exist: a different class from the streaming three, kept
|
|
358
|
+
* separate so the two reasons cannot be confused for one.
|
|
359
|
+
*
|
|
360
|
+
* These are not data keys. `mcpServers` introduces tools, and `PreToolUse` fires for MCP tools and
|
|
361
|
+
* inside subagents (observed, not assumed). So this widening enlarges the surface the gate must
|
|
362
|
+
* cover, and the streaming keys' reason ("it only selects what is emitted") is false of it.
|
|
363
|
+
*
|
|
364
|
+
* Nor are they shadowing lanes. The closed lanes share one property: they can answer a permission
|
|
365
|
+
* before the gate does. These answer nothing. What makes them safe is structural: `permissionHooks`
|
|
366
|
+
* registers `PreToolUse` with no `matcher`, so a tool this host has never heard of reaches exactly
|
|
367
|
+
* the same handler as `Bash`. Coverage is by construction rather than by enumeration, which is why a
|
|
368
|
+
* tool set the host cannot predict is still a tool set the host decides about.
|
|
369
|
+
*
|
|
370
|
+
* If a `matcher` were ever introduced, this classification stops being true, and these two keys
|
|
371
|
+
* become the first way to add a tool nothing decides about. That sentence is the whole reason this
|
|
372
|
+
* list exists as data instead of as a decision somebody made once.
|
|
373
|
+
*
|
|
374
|
+
* `strictMcpConfig` belongs here because it acts on the same surface, but it moves the opposite
|
|
375
|
+
* way: it only ever removes servers the host did not declare. It is the safe direction of the same
|
|
376
|
+
* lane, and grouping them keeps that visible.
|
|
377
|
+
*/
|
|
378
|
+
export const TOOL_SURFACE_OPTION_KEYS = [
|
|
379
|
+
'mcpServers',
|
|
380
|
+
'strictMcpConfig',
|
|
381
|
+
] as const satisfies readonly (keyof AgentProcessRequest)[];
|
|
382
|
+
|
|
383
|
+
/**
|
|
384
|
+
* The keys that decide where a transcript goes: a third class again, and the reason is not the
|
|
385
|
+
* other two lists' reason.
|
|
386
|
+
*
|
|
387
|
+
* These are a confidentiality and egress surface. The streaming keys widen what this host observes
|
|
388
|
+
* about its own session. The tool-surface keys widen what the agent may do, and are covered because
|
|
389
|
+
* the gate has no matcher. These do neither: they hand a live object every message the agent saw,
|
|
390
|
+
* and a store is by definition somewhere else. Filing them under "it only selects what is emitted"
|
|
391
|
+
* would put a false sentence inside the constant whose entire job is to make the reason checkable.
|
|
392
|
+
*
|
|
393
|
+
* Nor are they shadowing lanes. The closed lanes share one property: they can answer a permission
|
|
394
|
+
* before the gate does. A store answers nothing and is never consulted about whether a tool runs.
|
|
395
|
+
*
|
|
396
|
+
* What makes them safe is that a destination cannot be named from off-box, and it is structural
|
|
397
|
+
* rather than enforced: `SessionStore` is an object with methods, so it has no JSON representation
|
|
398
|
+
* and cannot arrive over the link. The only code that can supply one is the code that composes this
|
|
399
|
+
* host. That is the same shape of argument as the gate's no-matcher coverage (a property of what
|
|
400
|
+
* the type is, not of a check somebody remembered to write) and it is pinned by
|
|
401
|
+
* pins/persistence-egress.test.ts.
|
|
402
|
+
*
|
|
403
|
+
* If a store ever becomes constructible from data (a URL, a connection string, a descriptor the
|
|
404
|
+
* host resolves into a client), this classification stops being true, and these become the first
|
|
405
|
+
* way a transcript can be sent somewhere the operator did not choose. That sentence is why this list
|
|
406
|
+
* is data instead of a decision somebody made once.
|
|
407
|
+
*/
|
|
408
|
+
export const PERSISTENCE_OPTION_KEYS = [
|
|
409
|
+
'sessionStore',
|
|
410
|
+
'sessionStoreFlush',
|
|
411
|
+
] as const satisfies readonly (keyof AgentProcessRequest)[];
|
|
412
|
+
|
|
413
|
+
/**
|
|
414
|
+
* The keys that choose which agent runs and what it is told at the start: a fourth class, and its
|
|
415
|
+
* reason is not any of the other three's.
|
|
416
|
+
*
|
|
417
|
+
* These were absent, not narrowed, and that is the whole classification. `model` and
|
|
418
|
+
* `systemPrompt` were simply not in this type, so nothing was ever protecting them; there was no
|
|
419
|
+
* decision to reverse, only a capability nobody had wired. The eight `SHADOWING_LANES` are the
|
|
420
|
+
* opposite case: each was considered and closed. Opening a gap and re-opening a closed lane look
|
|
421
|
+
* identical in a diff, and this list is how they stop looking identical.
|
|
422
|
+
*
|
|
423
|
+
* Neither appears in any permission evaluation path. They do not pre-answer a call, do not
|
|
424
|
+
* redirect a tool name, do not load a rule file and are never consulted about whether a tool runs,
|
|
425
|
+
* which is exactly the property the closed lanes share and these do not. What they change is which
|
|
426
|
+
* weights answer and what standing instructions those weights start with.
|
|
427
|
+
*
|
|
428
|
+
* The cost, because a widening with no stated cost is a widening nobody checked: a controller that
|
|
429
|
+
* can set a system prompt can give the agent standing instructions this host will never see the
|
|
430
|
+
* effect of, since the SDK's init message does not report one. The gate still decides every tool
|
|
431
|
+
* call, so the boundary is unmoved, but "the host can state what this agent was told" is not a
|
|
432
|
+
* property this package has; `AgentInitFacts` is where that can be verified.
|
|
433
|
+
*
|
|
434
|
+
* The pin asserts every member is composable and is none of the eight, exactly as the other lists
|
|
435
|
+
* do.
|
|
436
|
+
*/
|
|
437
|
+
export const AGENT_SELECTION_OPTION_KEYS = [
|
|
438
|
+
'model',
|
|
439
|
+
'systemPrompt',
|
|
440
|
+
] as const satisfies readonly (keyof AgentProcessRequest)[];
|
|
441
|
+
|
|
442
|
+
/**
|
|
443
|
+
* The CLI-parity keys, and the list that re-opens a closed lane on purpose. `effort` was absent (a
|
|
444
|
+
* gap, like `model`). `permissionMode` was once a shadowing lane, considered and closed, and it is
|
|
445
|
+
* opened here by name, with the reason:
|
|
446
|
+
*
|
|
447
|
+
* a session through this host must expose what `claude` exposes, and bypass is a common default
|
|
448
|
+
* for an operator. The gate never depended on the mode: `PreToolUse` fires under every mode, so
|
|
449
|
+
* the boundary set is still held under `bypassPermissions`. What the mode changes is the CLI's
|
|
450
|
+
* own prompt flow, which this host's gate already answers.
|
|
451
|
+
*
|
|
452
|
+
* The other eight stay closed: they are rule files and pre-answers, which is a different thing from a
|
|
453
|
+
* posture chosen in the open. Pinned by `pins/permission-config.test.ts`.
|
|
454
|
+
*/
|
|
455
|
+
export const CLI_PARITY_OPTION_KEYS = [
|
|
456
|
+
'effort',
|
|
457
|
+
'permissionMode',
|
|
458
|
+
] as const satisfies readonly (keyof AgentProcessRequest)[];
|
|
459
|
+
|
|
460
|
+
/** A started agent process, in this package's terms. */
|
|
461
|
+
export interface AgentProcess {
|
|
462
|
+
/**
|
|
463
|
+
* Everything the agent emits, in order. One consumer.
|
|
464
|
+
*
|
|
465
|
+
* This is a narrowing wrapper, not the SDK's `Query`, and that is load-bearing. `query()`
|
|
466
|
+
* returns an object that IS an async generator AND carries `setPermissionMode`,
|
|
467
|
+
* `applyFlagSettings`, `setMcpServers` and `setMcpPermissionModeOverride` — four calls that change
|
|
468
|
+
* permission outcomes mid-session, after any construction-time check has run. Handing that object
|
|
469
|
+
* out under an `AsyncGenerator` annotation hides them from the compiler and from nobody else: one
|
|
470
|
+
* cast, or any plain JavaScript, reaches all four. So it is wrapped rather than annotated, and
|
|
471
|
+
* "the composed options cannot ship a shadowing setting" stays true without the words "unless you
|
|
472
|
+
* cast" attached to it.
|
|
473
|
+
*
|
|
474
|
+
* Where a mid-session control goes instead: a named method on this handle, beside `prompt`,
|
|
475
|
+
* `interrupt` and `close`. Never by widening this property back to the `Query`, and never by
|
|
476
|
+
* casting it; the point is that one file decides which of the SDK's controls this package
|
|
477
|
+
* offers.
|
|
478
|
+
*/
|
|
479
|
+
readonly messages: AsyncGenerator<SDKMessage, void>;
|
|
480
|
+
/** Queue a turn. Returns false once the process is closed. */
|
|
481
|
+
prompt(text: string): boolean;
|
|
482
|
+
/** Stop the current turn in band, leaving the session alive. */
|
|
483
|
+
interrupt(): Promise<void>;
|
|
484
|
+
/**
|
|
485
|
+
* The named mid-session controls this package offers (the doc above says where they go): the
|
|
486
|
+
* three members of `session_configure`, each the SDK's own streaming-input setter behind a method.
|
|
487
|
+
* `setPermissionMode` is here deliberately: the one permission mutator that is a posture, reached
|
|
488
|
+
* only from the wire through `readSessionConfigure`; the other three stay unreachable.
|
|
489
|
+
*/
|
|
490
|
+
setModel(model: string | null): Promise<void>;
|
|
491
|
+
setPermissionMode(mode: PermissionMode): Promise<void>;
|
|
492
|
+
setThinking(thinking: ThinkingConfig): Promise<void>;
|
|
493
|
+
/** End the process and release everything it holds. Idempotent. */
|
|
494
|
+
close(): void;
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
/**
|
|
498
|
+
* Facts the agent states about itself at start, lifted out of the SDK's init message.
|
|
499
|
+
*
|
|
500
|
+
* This is the per-spawn version receipt, and its shape is why it cannot rot: `cliVersion` arrives
|
|
501
|
+
* on the session's own stream, so there is no place to cache it even by accident. The CLI can
|
|
502
|
+
* auto-update underneath a long-running host, and a value read once at startup would then be a
|
|
503
|
+
* confident lie on every later session — which is the failure this receipt exists to catch.
|
|
504
|
+
*
|
|
505
|
+
* Absent is `null`, never an omitted property: this record is destined for the wire, where JSON has
|
|
506
|
+
* no `undefined` and an optional field makes "not reported" and "reported as unset" the same thing.
|
|
507
|
+
*/
|
|
508
|
+
export interface AgentInitFacts {
|
|
509
|
+
readonly sessionId: string;
|
|
510
|
+
readonly cliVersion: string;
|
|
511
|
+
readonly cwd: string;
|
|
512
|
+
readonly model: string;
|
|
513
|
+
readonly permissionMode: string;
|
|
514
|
+
/** Where the agent found its credentials. The evidence that ambient auth actually resolved. */
|
|
515
|
+
readonly apiKeySource: string;
|
|
516
|
+
readonly tools: readonly string[];
|
|
517
|
+
readonly skills: readonly string[];
|
|
518
|
+
readonly plugins: readonly AgentPluginFact[];
|
|
519
|
+
readonly capabilities: readonly string[];
|
|
520
|
+
/**
|
|
521
|
+
* The MCP servers this agent actually connected to, and what happened to each.
|
|
522
|
+
*
|
|
523
|
+
* The agent reports this on the init message. Without it, "did my server register?" is answered
|
|
524
|
+
* by inferring it from tool names, which cannot tell a server that failed to connect from one that
|
|
525
|
+
* connected and exposed no tools, and an HTTP or stdio server, unlike an in-process one, can fail
|
|
526
|
+
* to connect. With it, the answer is the agent's own word, per server, by name and status.
|
|
527
|
+
*
|
|
528
|
+
* The status string is carried verbatim and never interpreted here. It is the agent's vocabulary,
|
|
529
|
+
* not this package's, and mapping it onto this package's words would be a second translation
|
|
530
|
+
* site, the thing `state/observer.ts` exists to be the only one of.
|
|
531
|
+
*/
|
|
532
|
+
readonly mcpServers: readonly AgentMcpServerFact[];
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
/** One MCP server as the agent reported it at start. `status` is the SDK's word, carried as-is. */
|
|
536
|
+
export interface AgentMcpServerFact {
|
|
537
|
+
readonly name: string;
|
|
538
|
+
readonly status: string;
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
export interface AgentPluginFact {
|
|
542
|
+
readonly name: string;
|
|
543
|
+
readonly path: string;
|
|
544
|
+
readonly version: string | null;
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
/**
|
|
548
|
+
* The init message's facts, or null for every other message.
|
|
549
|
+
*
|
|
550
|
+
* Reading it here rather than above the seam keeps the SDK's message union on this side; the layer
|
|
551
|
+
* above branches on a plain record.
|
|
552
|
+
*/
|
|
553
|
+
export function readInitFacts(message: SDKMessage): AgentInitFacts | null {
|
|
554
|
+
if (message.type !== 'system' || message.subtype !== 'init') return null;
|
|
555
|
+
return {
|
|
556
|
+
sessionId: message.session_id,
|
|
557
|
+
cliVersion: message.claude_code_version,
|
|
558
|
+
cwd: message.cwd,
|
|
559
|
+
model: message.model,
|
|
560
|
+
permissionMode: message.permissionMode,
|
|
561
|
+
apiKeySource: message.apiKeySource,
|
|
562
|
+
tools: message.tools,
|
|
563
|
+
skills: message.skills,
|
|
564
|
+
// Guarded for the same reason as `mcp_servers` below: `plugins` is declared required and read
|
|
565
|
+
// with `.map(…)`, the identical shape, the identical runtime-wins argument, the identical
|
|
566
|
+
// `TypeError`. Every member of this class is guarded, not only the one observed failing.
|
|
567
|
+
plugins: (message.plugins ?? []).map((plugin) => ({
|
|
568
|
+
name: plugin.name,
|
|
569
|
+
path: plugin.path,
|
|
570
|
+
version: plugin.version ?? null,
|
|
571
|
+
})),
|
|
572
|
+
capabilities: message.capabilities ?? [],
|
|
573
|
+
// Guarded though the type says required. `mcp_servers` is declared non-optional on the init
|
|
574
|
+
// message, so `message.mcp_servers.map(…)` typechecks, and this package's rule is that the
|
|
575
|
+
// runtime wins over the types. An init message without the field would throw a TypeError inside
|
|
576
|
+
// `readInitFacts`, and the pump's own catch would convert the throw into `process_failed`: the
|
|
577
|
+
// session ends, and the reader's bug is reported as the agent process dying. `capabilities` one
|
|
578
|
+
// line up is guarded for the same reason, and it at least has `?` in the type as a warning.
|
|
579
|
+
//
|
|
580
|
+
// The cost of an unguarded read is misattribution, not silence. This read has exactly one
|
|
581
|
+
// caller, `HostedSession.#pump`, which calls it before and outside the per-listener try/catch,
|
|
582
|
+
// so nothing swallows it: the enclosing catch finishes the session as `process_failed`, closes
|
|
583
|
+
// the process, releases the registry entry, refuses every `whenLive` waiter and puts a
|
|
584
|
+
// transition to `ended` on the wire. `state/observer.ts` contains no catch at all, so no
|
|
585
|
+
// observer wrapper can eat the throw.
|
|
586
|
+
mcpServers: (message.mcp_servers ?? []).map((server) => ({ name: server.name, status: server.status })),
|
|
587
|
+
};
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
/**
|
|
591
|
+
* The message stream, and ONLY the message stream.
|
|
592
|
+
*
|
|
593
|
+
* `return` and `throw` are delegated, not just `next`. `for await…of` calls `iterator.return()`
|
|
594
|
+
* when the loop leaves early — a `break`, a `return`, or a throw inside the body — and that call is
|
|
595
|
+
* what lets the underlying query release the subprocess. A wrapper implementing only `next` would
|
|
596
|
+
* swallow it, leaking a session per abandoned loop with no error, no log and nothing null: exactly
|
|
597
|
+
* the silent-loss shape this package is built against. Pinned by agent-process.test.ts.
|
|
598
|
+
*
|
|
599
|
+
* Exported for that pin. It is not part of the package's public surface.
|
|
600
|
+
*/
|
|
601
|
+
export function messagesOf(source: AsyncGenerator<SDKMessage, void>): AsyncGenerator<SDKMessage, void> {
|
|
602
|
+
return {
|
|
603
|
+
next: (...args) => source.next(...args),
|
|
604
|
+
return: (value) => source.return(value),
|
|
605
|
+
throw: (error) => source.throw(error),
|
|
606
|
+
[Symbol.asyncIterator]() {
|
|
607
|
+
return this;
|
|
608
|
+
},
|
|
609
|
+
} as AsyncGenerator<SDKMessage, void>;
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
/** Start an agent process. The subprocess exists when this returns. */
|
|
613
|
+
export function startAgentProcess(request: AgentProcessRequest): AgentProcess {
|
|
614
|
+
const input = new AsyncQueue<SDKUserMessage>();
|
|
615
|
+
|
|
616
|
+
const options: Options = {
|
|
617
|
+
cwd: request.cwd,
|
|
618
|
+
env: request.env,
|
|
619
|
+
settingSources: [...request.settingSources],
|
|
620
|
+
includePartialMessages: request.includePartialMessages,
|
|
621
|
+
forwardSubagentText: request.forwardSubagentText,
|
|
622
|
+
...(request.thinking === null ? {} : { thinking: request.thinking }),
|
|
623
|
+
...(request.mcpServers === null ? {} : { mcpServers: { ...request.mcpServers } }),
|
|
624
|
+
...(request.strictMcpConfig === null ? {} : { strictMcpConfig: request.strictMcpConfig }),
|
|
625
|
+
// `persistSession` is deliberately never composed: the mirror runs after the local write, so a
|
|
626
|
+
// store cannot be combined with local persistence off. Leaving the SDK's default in place is
|
|
627
|
+
// what makes that combination unbuildable here rather than merely undocumented.
|
|
628
|
+
...(request.sessionStore === null ? {} : { sessionStore: request.sessionStore }),
|
|
629
|
+
...(request.sessionStoreFlush === null ? {} : { sessionStoreFlush: request.sessionStoreFlush }),
|
|
630
|
+
...(request.plugins === null ? {} : { plugins: [...request.plugins] }),
|
|
631
|
+
...(request.hooks === null ? {} : { hooks: request.hooks }),
|
|
632
|
+
...(request.resume === null ? {} : { resume: request.resume, forkSession: request.fork }),
|
|
633
|
+
...(request.onStderr === null ? {} : { stderr: request.onStderr }),
|
|
634
|
+
...(request.spawn === null ? {} : { spawnClaudeCodeProcess: request.spawn }),
|
|
635
|
+
// Omitted rather than passed as null when unset, like every other optional above: the SDK reads
|
|
636
|
+
// an absent key as "use the default" and a present-but-null one as a value it must interpret.
|
|
637
|
+
...(request.model === null ? {} : { model: request.model }),
|
|
638
|
+
...(request.systemPrompt === null ? {} : { systemPrompt: request.systemPrompt }),
|
|
639
|
+
...(request.effort === null ? {} : { effort: request.effort }),
|
|
640
|
+
...(request.permissionMode === null ? {} : { permissionMode: request.permissionMode }),
|
|
641
|
+
};
|
|
642
|
+
|
|
643
|
+
const running: Query = query({ prompt: input, options });
|
|
644
|
+
let closed = false;
|
|
645
|
+
|
|
646
|
+
return {
|
|
647
|
+
messages: messagesOf(running),
|
|
648
|
+
prompt(text: string): boolean {
|
|
649
|
+
if (closed) return false;
|
|
650
|
+
// `session_id` and `uuid` are optional on this type and deliberately left off: the agent
|
|
651
|
+
// stamps its own. Sending an empty string would be a PRESENT id that is blank, which is a
|
|
652
|
+
// different and worse thing than an absent one.
|
|
653
|
+
const message: SDKUserMessage = {
|
|
654
|
+
type: 'user',
|
|
655
|
+
message: { role: 'user', content: [{ type: 'text', text }] },
|
|
656
|
+
parent_tool_use_id: null,
|
|
657
|
+
};
|
|
658
|
+
input.push(message);
|
|
659
|
+
return true;
|
|
660
|
+
},
|
|
661
|
+
async interrupt(): Promise<void> {
|
|
662
|
+
if (closed) return;
|
|
663
|
+
// The SDK's receipt names which queued messages survived the interrupt. Not surfaced here:
|
|
664
|
+
// acting on it needs the turn model, which belongs to the layer that owns turns.
|
|
665
|
+
await running.interrupt();
|
|
666
|
+
},
|
|
667
|
+
async setModel(model: string | null): Promise<void> {
|
|
668
|
+
if (closed) return;
|
|
669
|
+
// `undefined`, never null: the SDK reads undefined as "the default" and null as a model named null.
|
|
670
|
+
await running.setModel(model ?? undefined);
|
|
671
|
+
},
|
|
672
|
+
async setPermissionMode(mode: PermissionMode): Promise<void> {
|
|
673
|
+
if (closed) return;
|
|
674
|
+
await running.setPermissionMode(mode);
|
|
675
|
+
},
|
|
676
|
+
async setThinking(thinking: ThinkingConfig): Promise<void> {
|
|
677
|
+
if (closed) return;
|
|
678
|
+
// The SDK's LIVE setter is the token cap, and on current models it is on/off: 0 = disabled,
|
|
679
|
+
// null = the default (adaptive). A fixed budget is REJECTED by Opus 5 / Sonnet 5 / Fable, so an
|
|
680
|
+
// `enabled` ask maps to adaptive rather than to a 400. The DISPLAY rides along: `summarized` when
|
|
681
|
+
// asked, because the models' default (`omitted`) streams thinking blocks with empty text — the
|
|
682
|
+
// "no thinking" an operator sees while paying for it.
|
|
683
|
+
const cap = thinking.type === 'disabled' ? 0 : null;
|
|
684
|
+
const display = thinking.type === 'disabled' ? undefined : thinking.display;
|
|
685
|
+
await running.setMaxThinkingTokens(cap, display);
|
|
686
|
+
},
|
|
687
|
+
close(): void {
|
|
688
|
+
if (closed) return;
|
|
689
|
+
closed = true;
|
|
690
|
+
input.end();
|
|
691
|
+
running.close();
|
|
692
|
+
},
|
|
693
|
+
};
|
|
694
|
+
}
|