@naswerks/periscope 1.0.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/CHANGELOG.md +33 -0
- package/LICENSE +21 -0
- package/README.md +146 -0
- package/SECURITY.md +315 -0
- package/dist/bin/command.d.ts +97 -0
- package/dist/bin/command.js +141 -0
- package/dist/bin/config.d.ts +7 -0
- package/dist/bin/config.js +90 -0
- package/dist/bin/login.d.ts +48 -0
- package/dist/bin/login.js +82 -0
- package/dist/bin/main.d.ts +33 -0
- package/dist/bin/main.js +173 -0
- package/dist/bin/pair.d.ts +28 -0
- package/dist/bin/pair.js +198 -0
- package/dist/bin/periscope.d.ts +3 -0
- package/dist/bin/periscope.js +4 -0
- package/dist/bin/posture.d.ts +64 -0
- package/dist/bin/posture.js +157 -0
- package/dist/bin/reconfigure.d.ts +64 -0
- package/dist/bin/reconfigure.js +195 -0
- package/dist/bin/serve.d.ts +103 -0
- package/dist/bin/serve.js +441 -0
- package/dist/bin/status.d.ts +11 -0
- package/dist/bin/status.js +49 -0
- package/dist/bin/workspaces.d.ts +109 -0
- package/dist/bin/workspaces.js +215 -0
- package/dist/control/backoff.d.ts +25 -0
- package/dist/control/backoff.js +20 -0
- package/dist/control/codec.d.ts +15 -0
- package/dist/control/codec.js +501 -0
- package/dist/control/credential.d.ts +23 -0
- package/dist/control/credential.js +8 -0
- package/dist/control/frames.d.ts +1180 -0
- package/dist/control/frames.js +441 -0
- package/dist/control/link-state.d.ts +32 -0
- package/dist/control/link-state.js +39 -0
- package/dist/control/link.d.ts +109 -0
- package/dist/control/link.js +736 -0
- package/dist/control/queue.d.ts +110 -0
- package/dist/control/queue.js +174 -0
- package/dist/control/seq.d.ts +49 -0
- package/dist/control/seq.js +52 -0
- package/dist/control/stream-routing.d.ts +200 -0
- package/dist/control/stream-routing.js +167 -0
- package/dist/control/stream.d.ts +43 -0
- package/dist/control/stream.js +64 -0
- package/dist/core/async-queue.d.ts +26 -0
- package/dist/core/async-queue.js +63 -0
- package/dist/core/failure.d.ts +22 -0
- package/dist/core/failure.js +53 -0
- package/dist/core/index.d.ts +18 -0
- package/dist/core/index.js +7 -0
- package/dist/core/keyed-turns.d.ts +28 -0
- package/dist/core/keyed-turns.js +44 -0
- package/dist/core/paths.d.ts +25 -0
- package/dist/core/paths.js +77 -0
- package/dist/core/refusal.d.ts +43 -0
- package/dist/core/refusal.js +399 -0
- package/dist/core/result.d.ts +22 -0
- package/dist/core/result.js +16 -0
- package/dist/core/time.d.ts +19 -0
- package/dist/core/time.js +25 -0
- package/dist/core/vocab.d.ts +23 -0
- package/dist/core/vocab.js +37 -0
- package/dist/core/workspace-id.d.ts +12 -0
- package/dist/core/workspace-id.js +12 -0
- package/dist/gate/command.d.ts +136 -0
- package/dist/gate/command.js +551 -0
- package/dist/gate/decision.d.ts +111 -0
- package/dist/gate/decision.js +97 -0
- package/dist/gate/escalate.d.ts +82 -0
- package/dist/gate/escalate.js +92 -0
- package/dist/gate/gate.d.ts +173 -0
- package/dist/gate/gate.js +417 -0
- package/dist/gate/index.d.ts +39 -0
- package/dist/gate/index.js +31 -0
- package/dist/gate/jail.d.ts +92 -0
- package/dist/gate/jail.js +131 -0
- package/dist/gate/local.d.ts +69 -0
- package/dist/gate/local.js +51 -0
- package/dist/gate/outcome.d.ts +72 -0
- package/dist/gate/outcome.js +91 -0
- package/dist/gate/shell.d.ts +36 -0
- package/dist/gate/shell.js +226 -0
- package/dist/host/agent-process.d.ts +452 -0
- package/dist/host/agent-process.js +382 -0
- package/dist/host/bulk-post.d.ts +44 -0
- package/dist/host/bulk-post.js +114 -0
- package/dist/host/claude-transcripts.d.ts +84 -0
- package/dist/host/claude-transcripts.js +339 -0
- package/dist/host/config-file.d.ts +60 -0
- package/dist/host/config-file.js +216 -0
- package/dist/host/git-facts.d.ts +7 -0
- package/dist/host/git-facts.js +106 -0
- package/dist/host/hooks.d.ts +56 -0
- package/dist/host/hooks.js +75 -0
- package/dist/host/host.d.ts +356 -0
- package/dist/host/host.js +1294 -0
- package/dist/host/index.d.ts +35 -0
- package/dist/host/index.js +22 -0
- package/dist/host/link-state-file.d.ts +18 -0
- package/dist/host/link-state-file.js +66 -0
- package/dist/host/loopback.d.ts +38 -0
- package/dist/host/loopback.js +122 -0
- package/dist/host/machine.d.ts +9 -0
- package/dist/host/machine.js +19 -0
- package/dist/host/mcp-server.d.ts +11 -0
- package/dist/host/mcp-server.js +48 -0
- package/dist/host/package-facts.d.ts +3 -0
- package/dist/host/package-facts.js +26 -0
- package/dist/host/paired-credential-store.d.ts +32 -0
- package/dist/host/paired-credential-store.js +112 -0
- package/dist/host/paths.d.ts +80 -0
- package/dist/host/paths.js +165 -0
- package/dist/host/repository-read.d.ts +33 -0
- package/dist/host/repository-read.js +201 -0
- package/dist/host/session-store.d.ts +53 -0
- package/dist/host/session-store.js +100 -0
- package/dist/host/sign-in.d.ts +90 -0
- package/dist/host/sign-in.js +239 -0
- package/dist/host/telemetry.d.ts +42 -0
- package/dist/host/telemetry.js +74 -0
- package/dist/host/token-cache.d.ts +62 -0
- package/dist/host/token-cache.js +185 -0
- package/dist/host/transcript-fs.d.ts +4 -0
- package/dist/host/transcript-fs.js +104 -0
- package/dist/host/wire-request.d.ts +80 -0
- package/dist/host/wire-request.js +196 -0
- package/dist/host/workspace-fs.d.ts +14 -0
- package/dist/host/workspace-fs.js +84 -0
- package/dist/host/workspace-trust.d.ts +23 -0
- package/dist/host/workspace-trust.js +80 -0
- package/dist/identity/authorize.d.ts +59 -0
- package/dist/identity/authorize.js +72 -0
- package/dist/identity/config.d.ts +98 -0
- package/dist/identity/config.js +130 -0
- package/dist/identity/credential.d.ts +91 -0
- package/dist/identity/credential.js +76 -0
- package/dist/identity/device-code.d.ts +115 -0
- package/dist/identity/device-code.js +134 -0
- package/dist/identity/index.d.ts +33 -0
- package/dist/identity/index.js +10 -0
- package/dist/identity/mode.d.ts +106 -0
- package/dist/identity/mode.js +69 -0
- package/dist/identity/paired-credential.d.ts +52 -0
- package/dist/identity/paired-credential.js +43 -0
- package/dist/identity/pkce.d.ts +50 -0
- package/dist/identity/pkce.js +94 -0
- package/dist/identity/store.d.ts +43 -0
- package/dist/identity/store.js +43 -0
- package/dist/identity/token.d.ts +58 -0
- package/dist/identity/token.js +149 -0
- package/dist/index.d.ts +45 -0
- package/dist/index.js +53 -0
- package/dist/mcp/descriptor.d.ts +59 -0
- package/dist/mcp/descriptor.js +14 -0
- package/dist/mcp/index.d.ts +14 -0
- package/dist/mcp/index.js +4 -0
- package/dist/mcp/schema.d.ts +37 -0
- package/dist/mcp/schema.js +175 -0
- package/dist/mcp/server.d.ts +107 -0
- package/dist/mcp/server.js +66 -0
- package/dist/persistence/entry.d.ts +74 -0
- package/dist/persistence/entry.js +105 -0
- package/dist/persistence/index.d.ts +22 -0
- package/dist/persistence/index.js +8 -0
- package/dist/persistence/key.d.ts +46 -0
- package/dist/persistence/key.js +33 -0
- package/dist/persistence/mirror.d.ts +71 -0
- package/dist/persistence/mirror.js +57 -0
- package/dist/persistence/receipt.d.ts +111 -0
- package/dist/persistence/receipt.js +85 -0
- package/dist/persistence/retention.d.ts +68 -0
- package/dist/persistence/retention.js +68 -0
- package/dist/persistence/store.d.ts +106 -0
- package/dist/persistence/store.js +86 -0
- package/dist/persistence/transition-log.d.ts +73 -0
- package/dist/persistence/transition-log.js +133 -0
- package/dist/protocol.d.ts +27 -0
- package/dist/protocol.js +12 -0
- package/dist/sessions/index.d.ts +8 -0
- package/dist/sessions/index.js +4 -0
- package/dist/sessions/registry.d.ts +186 -0
- package/dist/sessions/registry.js +190 -0
- package/dist/sessions/session.d.ts +178 -0
- package/dist/sessions/session.js +288 -0
- package/dist/sessions/spawn-env.d.ts +88 -0
- package/dist/sessions/spawn-env.js +277 -0
- package/dist/state/coverage.d.ts +308 -0
- package/dist/state/coverage.js +315 -0
- package/dist/state/index.d.ts +12 -0
- package/dist/state/index.js +7 -0
- package/dist/state/machine.d.ts +112 -0
- package/dist/state/machine.js +237 -0
- package/dist/state/model.d.ts +233 -0
- package/dist/state/model.js +214 -0
- package/dist/state/observer.d.ts +55 -0
- package/dist/state/observer.js +413 -0
- package/dist/state/reporter.d.ts +45 -0
- package/dist/state/reporter.js +35 -0
- package/dist/state/store.d.ts +56 -0
- package/dist/state/store.js +120 -0
- package/dist/telemetry/index.d.ts +11 -0
- package/dist/telemetry/index.js +2 -0
- package/dist/telemetry/usage.d.ts +78 -0
- package/dist/telemetry/usage.js +69 -0
- package/dist/workspace/git-worktree.d.ts +150 -0
- package/dist/workspace/git-worktree.js +417 -0
- package/dist/workspace/index.d.ts +9 -0
- package/dist/workspace/index.js +4 -0
- package/dist/workspace/plain-dir.d.ts +34 -0
- package/dist/workspace/plain-dir.js +90 -0
- package/dist/workspace/provider.d.ts +152 -0
- package/dist/workspace/provider.js +2 -0
- package/dist/workspace/worktree-porcelain.d.ts +29 -0
- package/dist/workspace/worktree-porcelain.js +100 -0
- package/docs/architecture.md +277 -0
- package/docs/configuration.md +187 -0
- package/docs/gate.md +219 -0
- package/docs/identity.md +107 -0
- package/docs/protocol.md +501 -0
- package/docs/state-machine.md +160 -0
- package/examples/README.md +141 -0
- package/examples/minimal-controller/controller.ts +38 -0
- package/examples/parallel-run-proof/permission-mode-probe.ts +129 -0
- package/examples/parallel-run-proof/run.ts +850 -0
- package/examples/test-controller/controller.ts +655 -0
- package/examples/test-controller/serve.ts +29 -0
- package/examples/tsconfig.json +30 -0
- package/package.json +98 -0
|
@@ -0,0 +1,1180 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The wire contract.
|
|
3
|
+
*
|
|
4
|
+
* Naming follows the Agent Client Protocol (agentclientprotocol.com, Zed Industries, Apache-2.0):
|
|
5
|
+
* its method names, camelCase keys and snake_case discriminators. Names and conventions only;
|
|
6
|
+
* every type here is written from scratch and derived from none of its artifacts.
|
|
7
|
+
*
|
|
8
|
+
* Two conventions this file holds to, both of which bite later if broken:
|
|
9
|
+
* - Absent is `null`, never `undefined`. JSON has no `undefined`, so an optional property makes
|
|
10
|
+
* "field omitted" and "field present and unset" indistinguishable across a round trip.
|
|
11
|
+
* - Every discriminator begins with a noun from core/vocab.ts.
|
|
12
|
+
*/
|
|
13
|
+
import type { Refusal } from '../core/refusal.js';
|
|
14
|
+
import type { SessionTransition } from '../state/model.js';
|
|
15
|
+
/**
|
|
16
|
+
* Bumped when a change is not backward-compatible. Exchanged in the hello handshake.
|
|
17
|
+
*
|
|
18
|
+
* The rule: a new payload kind, a new member on an existing kind, a member whose nullability
|
|
19
|
+
* changed, or a new hello member is a bump, even when the change is additive in practice. The
|
|
20
|
+
* handshake's job is to let a peer refuse a version it does not know rather than to guess whether
|
|
21
|
+
* a difference happened to be survivable: every declared member is required on the wire, so a
|
|
22
|
+
* peer one version behind refuses the whole frame, and because refusals are not acknowledged that
|
|
23
|
+
* refusal would be a permanent silent wedge on the session lane. The handshake refuses by version
|
|
24
|
+
* instead, by name, once. A new value in the open `capabilities` list is not a bump; a new
|
|
25
|
+
* `RefusalReason` is one, because a strict encoder on the other side must learn it first.
|
|
26
|
+
*
|
|
27
|
+
* Every bump re-approves `contracts/wire-vectors/` (`npm run contracts:update`) and regenerates
|
|
28
|
+
* any consumer's readers.
|
|
29
|
+
*/
|
|
30
|
+
export declare const PROTOCOL_VERSION = 9;
|
|
31
|
+
/**
|
|
32
|
+
* The oldest protocol version this build still speaks. A hello advertises the window
|
|
33
|
+
* `[PROTOCOL_VERSION_MIN, PROTOCOL_VERSION]` beside `protocolVersion`; the controller answers with
|
|
34
|
+
* its choice inside the overlap and the host accepts any version in its own window. The window
|
|
35
|
+
* opens at the first negotiated version; from the next bump on it is one minor wide, the version
|
|
36
|
+
* before the current one staying supported for one release. A hello with no range does not
|
|
37
|
+
* decode, so a version older than the first negotiated one cannot be inside the window.
|
|
38
|
+
*/
|
|
39
|
+
export declare const PROTOCOL_VERSION_MIN = 9;
|
|
40
|
+
/** The versions a peer speaks, inclusive at both ends. */
|
|
41
|
+
export interface ProtocolRange {
|
|
42
|
+
readonly min: number;
|
|
43
|
+
readonly max: number;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* A frame larger than this is refused by the codec.
|
|
47
|
+
*
|
|
48
|
+
* This is the mechanical half of "commands only, never payloads": bulk bytes cannot ride the link
|
|
49
|
+
* even by accident, because a frame carrying them will not encode. The refusal names the bulk lane
|
|
50
|
+
* so the failure teaches the fix rather than just reporting a size.
|
|
51
|
+
*/
|
|
52
|
+
export declare const MAX_FRAME_BYTES: number;
|
|
53
|
+
/**
|
|
54
|
+
* What a session update carries. ACP: `session/update`, whose payload is likewise a union.
|
|
55
|
+
*
|
|
56
|
+
* Three members. A later kind of update adds a member here rather than a new payload kind, which
|
|
57
|
+
* keeps `session_update` meaning "a settled fact about this session" (the non-droppable lane)
|
|
58
|
+
* instead of splitting one idea across several discriminators.
|
|
59
|
+
*
|
|
60
|
+
* `WireRefusalUpdate` is the third, and it is a body member rather than a new payload kind for
|
|
61
|
+
* exactly that reason. A host-side refusal that the controller must act on is a settled fact about
|
|
62
|
+
* the session, so it belongs on the non-droppable lane beside a transition. Adding a kind would
|
|
63
|
+
* also have meant a dispatcher change on both ends for a frame that carries no new idea.
|
|
64
|
+
*/
|
|
65
|
+
export type SessionUpdateBody = StateTransitionUpdate | AgentMessageUpdate | WireRefusalUpdate;
|
|
66
|
+
/**
|
|
67
|
+
* A declared state transition. The reason this lane is not droppable: losing one is not a repaint
|
|
68
|
+
* that can be redone, it is a lie about what the session did.
|
|
69
|
+
*/
|
|
70
|
+
export interface StateTransitionUpdate {
|
|
71
|
+
readonly update: 'state_transition';
|
|
72
|
+
readonly transition: SessionTransition;
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* One message the agent emitted, carried verbatim.
|
|
76
|
+
*
|
|
77
|
+
* Verbatim is the contract, not a shortcut. The SDK's own message shapes are this package's
|
|
78
|
+
* vocabulary, so forwarding them unchanged means there is exactly one place SDK models become this
|
|
79
|
+
* package's models: `state/observer.ts`, which says so in its first line. Normalizing here would be
|
|
80
|
+
* a second translation site, sitting where drift is hardest to see, and it would not remove the
|
|
81
|
+
* need to track the SDK anyway. A consumer reads `message.type` and gets the SDK's answer.
|
|
82
|
+
*
|
|
83
|
+
* Which messages ride here and which ride the delta lane is declared in `control/stream-routing.ts`,
|
|
84
|
+
* one row per discriminator. It is not a judgement made at the call site.
|
|
85
|
+
*/
|
|
86
|
+
export interface AgentMessageUpdate {
|
|
87
|
+
readonly update: 'agent_message';
|
|
88
|
+
/** The SDK message as it arrived. Opaque to this layer; `message.type` is the discriminator. */
|
|
89
|
+
readonly message: JsonObject;
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* A settled fact about a session. ACP: `session/update`.
|
|
93
|
+
*
|
|
94
|
+
* `body` is structurally open and read through a typed pair, which is a deliberate choice rather
|
|
95
|
+
* than a missing narrowing. Declaring `body: SessionUpdateBody` would reject a newer peer's richer
|
|
96
|
+
* body at the type level while the codec is built to carry unknown keys across intact — the two
|
|
97
|
+
* would disagree about the same field. So the contract lives in `stateTransitionUpdate` and
|
|
98
|
+
* `readStateTransition`: a producer cannot build a malformed one, a consumer cannot read one
|
|
99
|
+
* without checking, and the wire stays tolerant.
|
|
100
|
+
*/
|
|
101
|
+
export interface SessionUpdate {
|
|
102
|
+
readonly kind: 'session_update';
|
|
103
|
+
readonly body: JsonObject;
|
|
104
|
+
}
|
|
105
|
+
/** Build the update carrying a transition. The only supported way to put one on the wire. */
|
|
106
|
+
export declare function stateTransitionUpdate(transition: SessionTransition): SessionUpdate;
|
|
107
|
+
/**
|
|
108
|
+
* Read a transition back out, or null when the body is some other kind of update.
|
|
109
|
+
*
|
|
110
|
+
* Null rather than a throw: an older host receiving a body it does not model must keep going, and
|
|
111
|
+
* a consumer that gets null knows to leave the frame alone rather than to treat it as corrupt.
|
|
112
|
+
*/
|
|
113
|
+
export declare function readStateTransition(body: JsonObject): SessionTransition | null;
|
|
114
|
+
/** Build the update carrying an agent message. The only supported way to put one on the wire. */
|
|
115
|
+
export declare function agentMessageUpdate(message: JsonObject): SessionUpdate;
|
|
116
|
+
/** Read a forwarded message back out, or null when the body is some other kind of update. */
|
|
117
|
+
export declare function readAgentMessage(body: JsonObject): JsonObject | null;
|
|
118
|
+
/**
|
|
119
|
+
* A refusal the host decided about the controller's own traffic, sent back so the controller can act.
|
|
120
|
+
*
|
|
121
|
+
* The one this exists for is `seq-gap`, and it closes a hole that can only be seen from here.
|
|
122
|
+
* The receiver's `SeqTracker` advances only on an exact `last + 1`, so a frame arriving with a hole
|
|
123
|
+
* in front of it is dropped and `last` does not move; every later frame on that session is then
|
|
124
|
+
* also a gap. There is no NACK, no retransmit request and no resync frame, so without this the only
|
|
125
|
+
* signal is a host-local event: the sender, the single party able to fix it, is told nothing and
|
|
126
|
+
* the lane simply goes quiet forever. A quiet lane and a healthy idle lane look identical.
|
|
127
|
+
*
|
|
128
|
+
* `expected` is not diagnostic; it is the instruction. It is the exact seq the receiver will
|
|
129
|
+
* accept next, so a sender that re-sends from there heals the lane with no further protocol. That is
|
|
130
|
+
* why this carries two numbers instead of a message: a refusal a peer can act on beats one it can
|
|
131
|
+
* only log.
|
|
132
|
+
*
|
|
133
|
+
* It is not a new payload kind, deliberately. See `SessionUpdateBody`.
|
|
134
|
+
*/
|
|
135
|
+
export interface WireRefusalUpdate {
|
|
136
|
+
readonly update: 'wire_refusal';
|
|
137
|
+
readonly refusal: WireRefusal;
|
|
138
|
+
/** The seq the receiver will accept next. Re-send from here. */
|
|
139
|
+
readonly expected: number;
|
|
140
|
+
/** What actually arrived, so the size of the hole is legible without arithmetic. */
|
|
141
|
+
readonly received: number;
|
|
142
|
+
}
|
|
143
|
+
/** Build the update carrying a refusal about the peer's own traffic. The only supported way. */
|
|
144
|
+
export declare function wireRefusalUpdate(refusal: WireRefusal, expected: number, received: number): SessionUpdate;
|
|
145
|
+
/**
|
|
146
|
+
* Read a wire refusal back out, or null when the body is some other kind of update.
|
|
147
|
+
*
|
|
148
|
+
* Null rather than a throw, for the same reason `readStateTransition` returns null: a peer that does
|
|
149
|
+
* not model this member must keep going rather than treat the frame as corrupt. A v1 controller
|
|
150
|
+
* reaching this body gets null from all three readers and leaves the frame alone — which is a
|
|
151
|
+
* degraded outcome, not a broken one, and is the honest cost the version bump exists to announce.
|
|
152
|
+
*/
|
|
153
|
+
export declare function readWireRefusal(body: JsonObject): WireRefusalUpdate | null;
|
|
154
|
+
/**
|
|
155
|
+
* An incremental fragment: streamed tokens, thinking prose, progress tickers.
|
|
156
|
+
*
|
|
157
|
+
* The one droppable kind. A delta is superseded by the update that settles it, so losing one under
|
|
158
|
+
* pressure costs a repaint and never a fact. Which messages ride here is declared per discriminator
|
|
159
|
+
* in `control/stream-routing.ts`.
|
|
160
|
+
*
|
|
161
|
+
* The notify contract for consumers, which fails silently when broken: a consumer folding these
|
|
162
|
+
* into view state must return a new top-level state reference for every fold that changes
|
|
163
|
+
* anything, and the same reference for a true no-op. UI frameworks commonly bind rendered state
|
|
164
|
+
* through a reference-equality check, so a fold that mutates its state in place and returns the
|
|
165
|
+
* object it was given produces no notification: mid-turn painting stops dead and only resumes when
|
|
166
|
+
* something else happens to replace the reference, typically the turn's own result. There is no
|
|
167
|
+
* error, no dropped frame and nothing null; the frames arrive correctly and the view simply stops
|
|
168
|
+
* moving, which reads as a hung session.
|
|
169
|
+
*
|
|
170
|
+
* This is stated here rather than left to the consumer because it is inherited by whoever renders
|
|
171
|
+
* these frames; the symptom is a whole turn appearing in one lump.
|
|
172
|
+
*/
|
|
173
|
+
export interface SessionDelta {
|
|
174
|
+
readonly kind: 'session_delta';
|
|
175
|
+
readonly body: JsonObject;
|
|
176
|
+
}
|
|
177
|
+
/**
|
|
178
|
+
* Build a delta carrying one message verbatim.
|
|
179
|
+
*
|
|
180
|
+
* The body is the same shape a `session_update` carries, deliberately: the lanes differ in what
|
|
181
|
+
* losing a frame costs, not in what a frame says, so `readAgentMessage` reads either. A consumer
|
|
182
|
+
* that had to parse two shapes for one idea would have been handed the drift this package's
|
|
183
|
+
* vocabulary rules exist to prevent.
|
|
184
|
+
*/
|
|
185
|
+
export declare function agentMessageDelta(message: JsonObject): SessionDelta;
|
|
186
|
+
/**
|
|
187
|
+
* Start a session. ACP: `session/new`.
|
|
188
|
+
*
|
|
189
|
+
* This payload carries more than `cwd` so that a controller can ask for a session to be resumed,
|
|
190
|
+
* forked, given tools, or told which model to run over the wire, rather than having to bypass the
|
|
191
|
+
* link and call the in-process composer.
|
|
192
|
+
*
|
|
193
|
+
* The test for what belongs here, so the next addition is decided rather than argued: is this a
|
|
194
|
+
* decision about the session (resume this one, fork it, register these tools, use that model), or a
|
|
195
|
+
* capability handle (a store, a spawn function, a hook)? Decisions travel. Handles stay with the
|
|
196
|
+
* code that composes this host; see `SessionNewRequest`'s note on the five that stay refused.
|
|
197
|
+
*
|
|
198
|
+
* Every added field is `T | null` and null means "decide as you did before". A controller that
|
|
199
|
+
* sends only `cwd` gets the previous behaviour, which is what makes a widening safe to land under a
|
|
200
|
+
* host already running.
|
|
201
|
+
*/
|
|
202
|
+
export interface SessionNew {
|
|
203
|
+
readonly kind: 'session_new';
|
|
204
|
+
/**
|
|
205
|
+
* Where the session runs. Absolute when stated; null means "no ask".
|
|
206
|
+
*
|
|
207
|
+
* Advisory when the host has a workspace provider: the provider decides, and the session's
|
|
208
|
+
* first `spawning` transition carries `where.cwd`, the directory it actually got. A controller
|
|
209
|
+
* learns where its session is from that transition rather than assuming this value was honoured.
|
|
210
|
+
* Deliberately not echoed a second time anywhere: two names for one fact is how a vocabulary
|
|
211
|
+
* drifts apart, and this file has no `session_started` kind for exactly that reason.
|
|
212
|
+
*
|
|
213
|
+
* Null is "the provider decides", and nothing else. Under a provider it changes nothing;
|
|
214
|
+
* the ask was already ignored. With no provider there is nowhere honest to fall back to (the
|
|
215
|
+
* controller's own cwd is the weakest isolation this host knows, and recreating it as a default
|
|
216
|
+
* would smuggle a policy through an absence), so the host refuses `path-input-missing` by name.
|
|
217
|
+
*/
|
|
218
|
+
readonly cwd: string | null;
|
|
219
|
+
/**
|
|
220
|
+
* The key the workspace provider provisions AT, when it should not be the session key.
|
|
221
|
+
*
|
|
222
|
+
* Null is "use the session key", and nothing else: the 1:1 topology exactly as it was, so
|
|
223
|
+
* every existing caller is unchanged. A non-null key is what makes a shared workspace
|
|
224
|
+
* expressible: two sessions naming the same key resolve to the same directory (and, under the git
|
|
225
|
+
* provider, the same branch), which is the one-worktree-per-unit-of-work shape a session key can
|
|
226
|
+
* never spell because it is unique per session by construction.
|
|
227
|
+
*
|
|
228
|
+
* It becomes a git branch component and a directory segment, so the host validates it against
|
|
229
|
+
* the same union-of-two-rules screen the provider applies to a session key (`rejectUnusableId`),
|
|
230
|
+
* before any workspace is claimed; a key that passes a loose guard and dies inside git on every
|
|
231
|
+
* open is the defect class this prevents. With no workspace provider a non-null key is refused by
|
|
232
|
+
* name: there is nothing to provision at any key, and running in the controller's cwd instead
|
|
233
|
+
* would be the silent-wrong-topology failure this field exists to end (the `path-input-missing`
|
|
234
|
+
* posture's mirror image).
|
|
235
|
+
*/
|
|
236
|
+
readonly workspaceKey: string | null;
|
|
237
|
+
/**
|
|
238
|
+
* The controller's own meaning handle for this session, carried onto every transition.
|
|
239
|
+
*
|
|
240
|
+
* Never interpreted by the host. It exists so a controller can tie a session back to whatever
|
|
241
|
+
* it means on its side, and the moment the host parsed it, this package would know something about
|
|
242
|
+
* one product and stop being general. See the three-ids warning on `SessionFrame`.
|
|
243
|
+
*/
|
|
244
|
+
readonly correlationId: string | null;
|
|
245
|
+
/** Everything else the session is being asked for. Null means every default. */
|
|
246
|
+
readonly request: SessionNewRequest | null;
|
|
247
|
+
/** Per-session gate timings. Null means the host's own defaults. */
|
|
248
|
+
readonly gate: SessionNewGate | null;
|
|
249
|
+
}
|
|
250
|
+
/**
|
|
251
|
+
* The JSON-expressible subset of what a session can be asked for.
|
|
252
|
+
*
|
|
253
|
+
* Five keys of the in-process API are refused here and stay refused: the transcript mirror and
|
|
254
|
+
* its flush mode, the custom process spawner, the stderr callback and the hook registrations. That
|
|
255
|
+
* is a boundary, not an omission, and it is structural rather than enforced: every one of them is an
|
|
256
|
+
* object with methods or a function, so none has a JSON representation and none can cross a wire
|
|
257
|
+
* even in principle. The mirror is the sharpest: it receives every message the agent saw, so a
|
|
258
|
+
* controller able to name one could name a destination for a transcript. Nothing here can.
|
|
259
|
+
*
|
|
260
|
+
* They are described here and spelled out in `host/wire-request.ts`, which is deliberate rather
|
|
261
|
+
* than coy. `pins/persistence-egress.test.ts` scans this file's text for the mirror's key names and
|
|
262
|
+
* fails on a hit, so that a wire field for one cannot be added without the pin firing. Writing the
|
|
263
|
+
* names here, even inside a comment forbidding them, would trip that scan, and a pin loosened to
|
|
264
|
+
* tell prose from code is a pin with a new way to be wrong. The list lives one file over, at the
|
|
265
|
+
* only place that converts a wire request into a local one, and the pin keeps its teeth.
|
|
266
|
+
*/
|
|
267
|
+
export interface SessionNewRequest {
|
|
268
|
+
/** A session id to continue. Null starts a new one. */
|
|
269
|
+
readonly resume: string | null;
|
|
270
|
+
/** With `resume`, continue into a NEW session id instead of extending the old one. */
|
|
271
|
+
readonly fork: boolean | null;
|
|
272
|
+
/**
|
|
273
|
+
* Which on-disk settings tiers the agent may load. `[]` means none, which is the host's default.
|
|
274
|
+
*
|
|
275
|
+
* Asking for any tier can be refused before a process exists. A host that grants what its gate
|
|
276
|
+
* approves plus loaded operator settings is two authorities over one question with no stated
|
|
277
|
+
* precedence, so the composer refuses the pair by name (`permission-grant-shadows-settings`). The
|
|
278
|
+
* refusal arrives at start-up, naming the tiers, never as a session that runs with rules silently
|
|
279
|
+
* in conflict.
|
|
280
|
+
*
|
|
281
|
+
* Carried as plain strings: the narrow union lives with the SDK, on the far side of a boundary
|
|
282
|
+
* this file is not allowed to reach, so the host narrows and refuses an unknown value by name.
|
|
283
|
+
*/
|
|
284
|
+
readonly settingSources: readonly string[] | null;
|
|
285
|
+
readonly plugins: readonly SessionNewPlugin[] | null;
|
|
286
|
+
/**
|
|
287
|
+
* MCP servers this session may call, by server name.
|
|
288
|
+
*
|
|
289
|
+
* Every tool these add is decided by the same gate as `Bash`: the permission hook registers
|
|
290
|
+
* `PreToolUse` with no matcher, so a tool nobody predicted reaches the same handler. That is why
|
|
291
|
+
* a controller may register tools at all: coverage is by construction, not by enumeration.
|
|
292
|
+
*
|
|
293
|
+
* Carried opaquely because the config union is the SDK's. The host narrows it, and a name that
|
|
294
|
+
* collides with a server the host itself registered is refused rather than silently resolved.
|
|
295
|
+
*/
|
|
296
|
+
readonly mcpServers: JsonObject | null;
|
|
297
|
+
/**
|
|
298
|
+
* Ignore every MCP server the host did not declare. Null leaves the host's default, which is true.
|
|
299
|
+
*
|
|
300
|
+
* True is the default and turning it off is a real decision. With it off the agent also loads
|
|
301
|
+
* project `.mcp.json`, user settings, plugin MCP and on-disk agent frontmatter — so a repository
|
|
302
|
+
* a provider checked out becomes a tool-registration channel, in exactly the case where nobody
|
|
303
|
+
* reviewed what is in it.
|
|
304
|
+
*/
|
|
305
|
+
readonly strictMcpConfig: boolean | null;
|
|
306
|
+
/** Stream turns as they compose. Null leaves the host's default, which is ON. */
|
|
307
|
+
readonly includePartialMessages: boolean | null;
|
|
308
|
+
/** How much reasoning this session emits. Null leaves the SDK's own default. Opaque here. */
|
|
309
|
+
readonly thinking: JsonObject | null;
|
|
310
|
+
/**
|
|
311
|
+
* The effort level (`low` · `medium` · `high` · `xhigh` · `max`). Null leaves the SDK's own default.
|
|
312
|
+
* A plain string here; the host narrows it and refuses an unknown level by name.
|
|
313
|
+
*/
|
|
314
|
+
readonly effort: string | null;
|
|
315
|
+
/**
|
|
316
|
+
* The permission mode (`default` · `acceptEdits` · `bypassPermissions` · `plan` · `dontAsk` · `auto`).
|
|
317
|
+
* Null leaves the SDK's default. This lane is open by design (CLI parity): a session exposes what
|
|
318
|
+
* `claude` exposes. The gate's authority never rides the mode; it rides the `PreToolUse` hook,
|
|
319
|
+
* which the SDK fires under every mode, so the boundary set (push · remote surgery · branch
|
|
320
|
+
* delete · `gh pr merge`) is still held for a decision under bypass. What stays closed:
|
|
321
|
+
* `settings`, `managedSettings`, `allowedTools`, `disallowedTools`, `canUseTool`, `permissions`,
|
|
322
|
+
* `toolAliases`, `permissionPromptToolName`. A mode is a posture the operator chooses in the open;
|
|
323
|
+
* those are rule files and pre-answers nobody can see.
|
|
324
|
+
*/
|
|
325
|
+
readonly permissionMode: string | null;
|
|
326
|
+
/** Forward a subagent's whole conversation rather than only its tool calls. Null means OFF. */
|
|
327
|
+
readonly forwardSubagentText: boolean | null;
|
|
328
|
+
/**
|
|
329
|
+
* Extra allow / deny / literal declarations on top of the host's spawn environment.
|
|
330
|
+
*
|
|
331
|
+
* What survives an off-box widening is a short, named list, and it is not "whatever the host
|
|
332
|
+
* refuses". Two facts in `sessions/spawn-env.ts` are true and are worth having: an extra allowed
|
|
333
|
+
* key cannot override a denied one, and the host-session strip runs last, after the literals. What
|
|
334
|
+
* does not follow from them is that a variable the host declines to carry into a child cannot be
|
|
335
|
+
* re-admitted from off-box. It can.
|
|
336
|
+
*
|
|
337
|
+
* The host's posture is an allow-list: everything undeclared is absent, and `extraAllowedKeys`
|
|
338
|
+
* re-admits any of those by name. Further, `extraEnv` is applied after the filter with no allow
|
|
339
|
+
* or deny check at all. That is deliberate, because setting a value is a different act from
|
|
340
|
+
* inheriting one and was designed for an embedder with an author. Carrying it here makes the
|
|
341
|
+
* author a controller instead. So `NODE_OPTIONS`, `PATH`, `ANTHROPIC_BASE_URL` and even
|
|
342
|
+
* `NODE_TLS_REJECT_UNAUTHORIZED` (which the host refuses to inherit by explicit pattern) are all
|
|
343
|
+
* settable through this field.
|
|
344
|
+
*
|
|
345
|
+
* Irrevocable, and exhaustive: the 2 `DENIED_PATTERNS` (only against inheritance, not against
|
|
346
|
+
* `extraEnv`) and the 15 `HOST_SESSION_MARKERS`, which are stripped last and unconditionally. The
|
|
347
|
+
* marker set is the one real guarantee: a spawned agent is never told it is a continuation of the
|
|
348
|
+
* host process, whatever any of these three arrays says.
|
|
349
|
+
*
|
|
350
|
+
* This is disclosure, not an argument against the widening, which is the point of carrying `env`
|
|
351
|
+
* at all. Narrowing it later is a versioned break, so an embedder that wants a floor beneath a
|
|
352
|
+
* controller's declarations needs one built for it; none exists in this package.
|
|
353
|
+
*/
|
|
354
|
+
readonly env: SessionNewEnv | null;
|
|
355
|
+
/**
|
|
356
|
+
* Which model runs. Null leaves the CLI's default.
|
|
357
|
+
*
|
|
358
|
+
* This and `systemPrompt` select what the process emits; neither can answer a permission, so
|
|
359
|
+
* carrying them does not widen a security narrowing. The eight `SHADOWING_LANES` that can answer
|
|
360
|
+
* a permission before the gate does are a deliberate narrowing and they stay closed. Two different
|
|
361
|
+
* facts; see `AGENT_SELECTION_OPTION_KEYS` in host/agent-process.ts for the checked version of
|
|
362
|
+
* this sentence.
|
|
363
|
+
*/
|
|
364
|
+
readonly model: string | null;
|
|
365
|
+
/**
|
|
366
|
+
* What the agent is told at the start. Null leaves the CLI's own preset.
|
|
367
|
+
*
|
|
368
|
+
* Not provable from the host's report. The SDK's init message carries `model`, `tools`,
|
|
369
|
+
* `mcp_servers`, `skills`, `plugins` and more, and no system prompt, so unlike every other field
|
|
370
|
+
* here, a controller cannot confirm from the host's own report that this took effect. It ships
|
|
371
|
+
* unproven-by-report, stated rather than discovered. Carried opaquely because the SDK accepts a
|
|
372
|
+
* string, a list of strings, or a preset object.
|
|
373
|
+
*/
|
|
374
|
+
readonly systemPrompt: JsonValue | null;
|
|
375
|
+
}
|
|
376
|
+
/**
|
|
377
|
+
* Build a `session_new`. The only supported way to put one on the wire.
|
|
378
|
+
*
|
|
379
|
+
* It exists for the reason `stateTransitionUpdate` does, and for one more. The wire's rule is that
|
|
380
|
+
* absent is `null` and never an omitted property, but a hand-written literal makes that a
|
|
381
|
+
* discipline every author has to remember, and the moment one is forgotten the frame carries
|
|
382
|
+
* `undefined`, which `JSON.stringify` deletes. The field then arrives absent, and "absent" and
|
|
383
|
+
* "explicitly unset" are the two states this file spends its opening paragraph refusing to
|
|
384
|
+
* conflate. Passing `asked` as a partial and filling every remaining key with `null` here makes
|
|
385
|
+
* the rule a property of construction instead of a rule in a comment.
|
|
386
|
+
*
|
|
387
|
+
* The argument is a partial of the wire type; what comes out is fully populated. So a caller writes
|
|
388
|
+
* only what it means, and no caller can produce a frame that says nothing where it meant nothing.
|
|
389
|
+
*/
|
|
390
|
+
export declare function sessionNew(cwd: string | null, asked?: Partial<Omit<SessionNew, 'kind' | 'cwd'>>): SessionNew;
|
|
391
|
+
/**
|
|
392
|
+
* Build a `session_new.request`. Same argument as `sessionNew`: every unstated key becomes `null`.
|
|
393
|
+
*
|
|
394
|
+
* The partial is the convenience and the full object is the contract. Nothing about the wire is
|
|
395
|
+
* relaxed here: a controller in another language builds the whole object, and this is the shortcut
|
|
396
|
+
* for the one that happens to be written in this one.
|
|
397
|
+
*/
|
|
398
|
+
export declare function sessionNewRequest(asked?: Partial<SessionNewRequest>): SessionNewRequest;
|
|
399
|
+
/**
|
|
400
|
+
* Every key a `session_new.request` carries, as data, so a check can enumerate them at run time.
|
|
401
|
+
*
|
|
402
|
+
* It exists so a pin cannot quietly stop covering something. The egress pin walks a populated
|
|
403
|
+
* request looking for a field that could name a transcript destination, and a walk is only as
|
|
404
|
+
* complete as the object it is handed: a hand-built fixture silently stops being full the moment a
|
|
405
|
+
* later change adds a key, and the pin then passes while covering less, with nothing to say so.
|
|
406
|
+
*
|
|
407
|
+
* `satisfies Record<keyof SessionNewRequest, true>` makes the compiler the enforcer: adding a field
|
|
408
|
+
* to the type without adding it here does not build. So the pin's fixture is checked against a list
|
|
409
|
+
* that cannot fall behind the type it describes.
|
|
410
|
+
*/
|
|
411
|
+
export declare const SESSION_NEW_REQUEST_KEYS: {
|
|
412
|
+
readonly resume: true;
|
|
413
|
+
readonly fork: true;
|
|
414
|
+
readonly settingSources: true;
|
|
415
|
+
readonly plugins: true;
|
|
416
|
+
readonly mcpServers: true;
|
|
417
|
+
readonly strictMcpConfig: true;
|
|
418
|
+
readonly includePartialMessages: true;
|
|
419
|
+
readonly thinking: true;
|
|
420
|
+
readonly forwardSubagentText: true;
|
|
421
|
+
readonly env: true;
|
|
422
|
+
readonly model: true;
|
|
423
|
+
readonly systemPrompt: true;
|
|
424
|
+
readonly effort: true;
|
|
425
|
+
readonly permissionMode: true;
|
|
426
|
+
};
|
|
427
|
+
/** The same, for the payload itself. Same argument, one level up. */
|
|
428
|
+
export declare const SESSION_NEW_KEYS: {
|
|
429
|
+
readonly kind: true;
|
|
430
|
+
readonly cwd: true;
|
|
431
|
+
readonly workspaceKey: true;
|
|
432
|
+
readonly correlationId: true;
|
|
433
|
+
readonly request: true;
|
|
434
|
+
readonly gate: true;
|
|
435
|
+
};
|
|
436
|
+
/** A plugin the session should load. Mirrors the SDK's shape; the host narrows it. */
|
|
437
|
+
export interface SessionNewPlugin {
|
|
438
|
+
readonly type: string;
|
|
439
|
+
readonly path: string;
|
|
440
|
+
readonly skipMcpDiscovery: boolean | null;
|
|
441
|
+
}
|
|
442
|
+
/** Extra environment declarations for the spawn. See `SessionNewRequest.env`. */
|
|
443
|
+
export interface SessionNewEnv {
|
|
444
|
+
readonly extraAllowedKeys: readonly string[] | null;
|
|
445
|
+
readonly extraDeniedKeys: readonly string[] | null;
|
|
446
|
+
readonly extraEnv: Readonly<Record<string, string>> | null;
|
|
447
|
+
}
|
|
448
|
+
/**
|
|
449
|
+
* Per-session gate timings. Three timeouts, and deliberately nothing else.
|
|
450
|
+
*
|
|
451
|
+
* `grantOnAllow` is not here, and its absence is a decision rather than an oversight. It is a
|
|
452
|
+
* permission posture, not a timing: turned off it yields a gate that can refuse a call and cannot
|
|
453
|
+
* approve one, so a controller able to set it could disable its own session's tools from off-box
|
|
454
|
+
* while every check still reported healthy. Timings decide how long the host waits; this decides
|
|
455
|
+
* whether an answer means anything. Different questions, and only the first travels.
|
|
456
|
+
*
|
|
457
|
+
* The two-deadline invariant is enforced: the host's own deadline must expire before the
|
|
458
|
+
* matcher's, and a pair that inverts it is refused at composition. It is reachable from the wire,
|
|
459
|
+
* which is why the refusal happens before any process exists rather than at the first tool call.
|
|
460
|
+
*/
|
|
461
|
+
export interface SessionNewGate {
|
|
462
|
+
readonly decisionTimeoutMs: number | null;
|
|
463
|
+
readonly holdAfterMs: number | null;
|
|
464
|
+
readonly matcherTimeoutSeconds: number | null;
|
|
465
|
+
}
|
|
466
|
+
/** Send a turn into a session. ACP: `session/prompt`. */
|
|
467
|
+
export interface SessionPrompt {
|
|
468
|
+
readonly kind: 'session_prompt';
|
|
469
|
+
readonly text: string;
|
|
470
|
+
}
|
|
471
|
+
/** Interrupt the current turn. ACP: `session/cancel`. */
|
|
472
|
+
export interface SessionCancel {
|
|
473
|
+
readonly kind: 'session_cancel';
|
|
474
|
+
}
|
|
475
|
+
/**
|
|
476
|
+
* Change a running session's model, permission mode or thinking: the SDK's streaming-input setters
|
|
477
|
+
* (`setModel` · `setPermissionMode` · `setMaxThinkingTokens`), reached over the wire. Each member
|
|
478
|
+
* is "not asked" when null; the host applies the asked ones in order and reports a failure by name.
|
|
479
|
+
*/
|
|
480
|
+
export interface SessionConfigure {
|
|
481
|
+
readonly kind: 'session_configure';
|
|
482
|
+
/** The model id to switch to. Null = not asked. */
|
|
483
|
+
readonly model: string | null;
|
|
484
|
+
/** The permission mode to switch to — the same vocabulary as `SessionNewRequest.permissionMode`. */
|
|
485
|
+
readonly permissionMode: string | null;
|
|
486
|
+
/** `{type:'adaptive'}` · `{type:'disabled'}` · `{type:'enabled', budgetTokens}` — the SDK's own shapes. */
|
|
487
|
+
readonly thinking: JsonObject | null;
|
|
488
|
+
}
|
|
489
|
+
/**
|
|
490
|
+
* Ask for bulk content. The link carries this locator, never the content.
|
|
491
|
+
*
|
|
492
|
+
* The host answers with an outbound HTTP POST to `postUrl` and reports the outcome back over the
|
|
493
|
+
* link. `bulk` is this package's noun, not ACP's; ACP has no equivalent concept, so nothing is
|
|
494
|
+
* being renamed.
|
|
495
|
+
*/
|
|
496
|
+
export interface BulkRequest {
|
|
497
|
+
readonly kind: 'bulk_request';
|
|
498
|
+
readonly deliveryId: string;
|
|
499
|
+
readonly what: string;
|
|
500
|
+
readonly fromOffset: number;
|
|
501
|
+
readonly postUrl: string;
|
|
502
|
+
}
|
|
503
|
+
/**
|
|
504
|
+
* The bytes were POSTed. A receipt, so it is not droppable.
|
|
505
|
+
*
|
|
506
|
+
* `sizeBytes` / `mtimeMs`: the delivered file's stat at the moment it was streamed, so a
|
|
507
|
+
* caller pulling a transcript can detect the CLI rewriting the file under it. The CLI rewrites
|
|
508
|
+
* transcripts on compaction, which makes a byte-offset resume across a rewrite invalid. Both are
|
|
509
|
+
* `T | null` (the wire's optionality rule): null means the deliverer did not read a stat, which is
|
|
510
|
+
* exactly what an older host sends, so a v3 receipt decodes as a v4 receipt with the two unknowns
|
|
511
|
+
* stated rather than invented.
|
|
512
|
+
*/
|
|
513
|
+
export interface BulkDelivered {
|
|
514
|
+
readonly kind: 'bulk_delivered';
|
|
515
|
+
readonly deliveryId: string;
|
|
516
|
+
readonly byteCount: number;
|
|
517
|
+
/** The file's total size when the delivery was read. Null when the deliverer did not stat it. */
|
|
518
|
+
readonly sizeBytes: number | null;
|
|
519
|
+
/** The file's mtime (integer epoch ms) when the delivery was read. Null when unknown. */
|
|
520
|
+
readonly mtimeMs: number | null;
|
|
521
|
+
}
|
|
522
|
+
/** Build a `bulk_delivered`. Fills the stat pair with null so an absent value is stated, not deleted. */
|
|
523
|
+
export declare function bulkDelivered(deliveryId: string, byteCount: number, stat?: {
|
|
524
|
+
sizeBytes?: number;
|
|
525
|
+
mtimeMs?: number;
|
|
526
|
+
}): BulkDelivered;
|
|
527
|
+
/** The delivery did not happen, and this says which named way it failed. */
|
|
528
|
+
export interface BulkFailed {
|
|
529
|
+
readonly kind: 'bulk_failed';
|
|
530
|
+
readonly deliveryId: string;
|
|
531
|
+
readonly refusal: WireRefusal;
|
|
532
|
+
}
|
|
533
|
+
/** Ask which sessions this host is running. */
|
|
534
|
+
export interface SessionList {
|
|
535
|
+
readonly kind: 'session_list';
|
|
536
|
+
readonly requestId: string;
|
|
537
|
+
}
|
|
538
|
+
/**
|
|
539
|
+
* One running session, as this host knows it.
|
|
540
|
+
*
|
|
541
|
+
* `sessionKey` is the controller's own routing handle (see the three-ids note on `SessionFrame`),
|
|
542
|
+
* the one id the asker can act on. `sessionId` is the agent's own id, null until the agent has
|
|
543
|
+
* named itself; `state` is the session lifecycle word as the host holds it, carried as a plain
|
|
544
|
+
* string so a newer host's added state survives the crossing.
|
|
545
|
+
*/
|
|
546
|
+
export interface SessionListEntry {
|
|
547
|
+
readonly sessionKey: string;
|
|
548
|
+
readonly sessionId: string | null;
|
|
549
|
+
readonly state: string;
|
|
550
|
+
readonly cwd: string | null;
|
|
551
|
+
readonly startedAt: string | null;
|
|
552
|
+
}
|
|
553
|
+
/** The answer: every session behind a handle, plus the registry's own two counts. */
|
|
554
|
+
export interface SessionListResult {
|
|
555
|
+
readonly kind: 'session_list_result';
|
|
556
|
+
readonly requestId: string;
|
|
557
|
+
readonly sessions: readonly SessionListEntry[];
|
|
558
|
+
readonly liveCount: number;
|
|
559
|
+
/** Sessions started but not yet self-named. Counted, not listable: they have no agent id yet. */
|
|
560
|
+
readonly provisioningCount: number;
|
|
561
|
+
}
|
|
562
|
+
/** Ask which Claude sessions exist on this machine, from `fromIndex`, newest first. */
|
|
563
|
+
export interface TranscriptList {
|
|
564
|
+
readonly kind: 'transcript_list';
|
|
565
|
+
readonly requestId: string;
|
|
566
|
+
readonly fromIndex: number;
|
|
567
|
+
}
|
|
568
|
+
/**
|
|
569
|
+
* One transcript on disk: an opaque project slug, the session id, the stat pair, and the working directory
|
|
570
|
+
* the CLI recorded on it — the cwd a resume of this transcript must run in (transcripts live per cwd).
|
|
571
|
+
* Null when the file's head carries none.
|
|
572
|
+
*/
|
|
573
|
+
export interface TranscriptListEntry {
|
|
574
|
+
readonly projectSlug: string;
|
|
575
|
+
readonly sessionId: string;
|
|
576
|
+
readonly sizeBytes: number;
|
|
577
|
+
readonly mtimeMs: number;
|
|
578
|
+
readonly cwd: string | null;
|
|
579
|
+
}
|
|
580
|
+
/**
|
|
581
|
+
* One page of the enumeration. Paged because the answer must fit `MAX_FRAME_BYTES`: a machine's
|
|
582
|
+
* corpus reaches hundreds of sessions, and one frame carrying all of them would be refused by the
|
|
583
|
+
* very codec that carries it. `nextIndex` null means this page ends the listing; re-ask from it
|
|
584
|
+
* otherwise. An empty first page is a fresh machine, not an error.
|
|
585
|
+
*/
|
|
586
|
+
export interface TranscriptListResult {
|
|
587
|
+
readonly kind: 'transcript_list_result';
|
|
588
|
+
readonly requestId: string;
|
|
589
|
+
readonly entries: readonly TranscriptListEntry[];
|
|
590
|
+
readonly totalCount: number;
|
|
591
|
+
readonly nextIndex: number | null;
|
|
592
|
+
}
|
|
593
|
+
/**
|
|
594
|
+
* Probe one transcript's tail: has a matching user entry landed at or past `fromOffset`?
|
|
595
|
+
*
|
|
596
|
+
* `needle` null means "any user-text entry". The answer carries the file's current size as the
|
|
597
|
+
* offset to resume from, plus the stat pair for rewrite detection.
|
|
598
|
+
*/
|
|
599
|
+
export interface TranscriptTail {
|
|
600
|
+
readonly kind: 'transcript_tail';
|
|
601
|
+
readonly requestId: string;
|
|
602
|
+
readonly projectSlug: string;
|
|
603
|
+
readonly sessionId: string;
|
|
604
|
+
readonly fromOffset: number;
|
|
605
|
+
readonly needle: string | null;
|
|
606
|
+
}
|
|
607
|
+
/**
|
|
608
|
+
* The probe's answer. `absent` is a value, not a failure: a transcript that does not exist is a
|
|
609
|
+
* real negative the asker may trust, and it must not wear a refusal's clothes; a refusal here
|
|
610
|
+
* names a malformed request, never a missing file. When `absent` is true the stat pair is null.
|
|
611
|
+
*/
|
|
612
|
+
export interface TranscriptTailResult {
|
|
613
|
+
readonly kind: 'transcript_tail_result';
|
|
614
|
+
readonly requestId: string;
|
|
615
|
+
readonly found: boolean;
|
|
616
|
+
readonly absent: boolean;
|
|
617
|
+
/** The file's current size — resume the next probe from here. 0 when absent. */
|
|
618
|
+
readonly newOffset: number;
|
|
619
|
+
readonly sizeBytes: number | null;
|
|
620
|
+
readonly mtimeMs: number | null;
|
|
621
|
+
}
|
|
622
|
+
/**
|
|
623
|
+
* A discovery request that could not be answered, and which named way it failed. The `bulk_failed`
|
|
624
|
+
* model: a request that dies in the host must say so on the wire, because to the asker a swallowed
|
|
625
|
+
* failure and a host that hung are the same silence.
|
|
626
|
+
*/
|
|
627
|
+
export interface TranscriptFailed {
|
|
628
|
+
readonly kind: 'transcript_failed';
|
|
629
|
+
readonly requestId: string;
|
|
630
|
+
readonly refusal: WireRefusal;
|
|
631
|
+
}
|
|
632
|
+
/**
|
|
633
|
+
* Ask this host to remove a workspace's directory from disk. Host-scoped, like the discovery
|
|
634
|
+
* asks: the routing key is a channel the controller chose, and no session needs to exist behind it.
|
|
635
|
+
*
|
|
636
|
+
* This is the one production path to `release(remove: true)`, and it is on-demand by design. A
|
|
637
|
+
* session ending does not remove its directory; `ReleaseOptions.remove` defaults false because a
|
|
638
|
+
* session that ended badly is one whose directory somebody wants to look at. Removal is something a
|
|
639
|
+
* caller asks for (a UI control, a PR-close hook), never something that happens because a session
|
|
640
|
+
* ended, so it arrives as its own command rather than as a flag remembered from `session_new`;
|
|
641
|
+
* a flag would spend the decision at session start, where no button and no webhook can ever act.
|
|
642
|
+
*
|
|
643
|
+
* The branch is deleted only when the ask says so (`deleteBranch`), and an unmerged branch only
|
|
644
|
+
* when the ask also says `force`; that is the provider's own rule. Removing a directory is
|
|
645
|
+
* reversible (the commits are still on the branch); deleting a branch is not, so it is opt-in per
|
|
646
|
+
* ask and never a standing setting.
|
|
647
|
+
*
|
|
648
|
+
* A worktree is addressed by its key OR by its path — exactly one. A key is what the controller
|
|
649
|
+
* asked for at `session_new`; a path is what the host observed, and the one name a controller holds
|
|
650
|
+
* for a worktree whose session asked for no key. The host resolves a path to the key of the
|
|
651
|
+
* directory directly under its workspace root, and refuses any other path by name.
|
|
652
|
+
*/
|
|
653
|
+
export interface WorkspaceRelease {
|
|
654
|
+
readonly kind: 'workspace_release';
|
|
655
|
+
readonly requestId: string;
|
|
656
|
+
/** The key the workspace was provisioned at, or null when `path` names it. */
|
|
657
|
+
readonly workspaceKey: string | null;
|
|
658
|
+
/** The worktree's absolute path, or null when `workspaceKey` names it. */
|
|
659
|
+
readonly path: string | null;
|
|
660
|
+
/** Delete the branch the worktree is on, after the directory. */
|
|
661
|
+
readonly deleteBranch: boolean;
|
|
662
|
+
/** Delete the branch even when it is not merged into the repository's default branch. */
|
|
663
|
+
readonly force: boolean;
|
|
664
|
+
}
|
|
665
|
+
/**
|
|
666
|
+
* The reap's answer, and every exit is this one kind.
|
|
667
|
+
*
|
|
668
|
+
* `refusal: null` means the workspace was released and its directory removed; a named refusal
|
|
669
|
+
* rides the same kind otherwise: an unusable key, a workspace still backing a live session, a
|
|
670
|
+
* provider failure, a host with no provider at all. One kind for every exit is deliberate rather
|
|
671
|
+
* than the bulk lane's success/failure pair: an ask whose answer dies inside the host is a silent
|
|
672
|
+
* failure on this lane, and a single result kind makes "every exit is a wire answer" a property of
|
|
673
|
+
* the shape instead of a discipline across two builders. It also keeps the answer one small frame,
|
|
674
|
+
* nowhere near the size cap: keys are bounded at `MAX_WORKSPACE_ID_LENGTH` and every refusal echoes
|
|
675
|
+
* at most `keyPreview`, so no key, however degenerate, can inflate this answer toward the cap.
|
|
676
|
+
*/
|
|
677
|
+
/**
|
|
678
|
+
* What one release did, entry by entry. The two flags are the receipt: a refusal with
|
|
679
|
+
* `directoryRemoved: true` is a partial (the directory went, the branch stayed), stated rather
|
|
680
|
+
* than hidden. `refusal: null` with both flags false is the idempotent answer — already absent.
|
|
681
|
+
*/
|
|
682
|
+
export interface WorkspaceReleaseEntryResult {
|
|
683
|
+
/** The key the ask resolved to; null when it named nothing this host could resolve. */
|
|
684
|
+
readonly workspaceKey: string | null;
|
|
685
|
+
/** The worktree's path as the provider knows it; null when unknown. */
|
|
686
|
+
readonly path: string | null;
|
|
687
|
+
/** A directory existed and is gone. */
|
|
688
|
+
readonly directoryRemoved: boolean;
|
|
689
|
+
readonly branchDeleted: boolean;
|
|
690
|
+
/** Null = released, or already absent. Otherwise the named reason, `branch-not-merged` included. */
|
|
691
|
+
readonly refusal: WireRefusal | null;
|
|
692
|
+
}
|
|
693
|
+
export interface WorkspaceReleaseResult extends WorkspaceReleaseEntryResult {
|
|
694
|
+
readonly kind: 'workspace_release_result';
|
|
695
|
+
readonly requestId: string;
|
|
696
|
+
}
|
|
697
|
+
/** One release in a `workspace_release_bulk`: the single ask's members without the envelope. */
|
|
698
|
+
export interface WorkspaceReleaseEntry {
|
|
699
|
+
readonly workspaceKey: string | null;
|
|
700
|
+
readonly path: string | null;
|
|
701
|
+
readonly deleteBranch: boolean;
|
|
702
|
+
readonly force: boolean;
|
|
703
|
+
}
|
|
704
|
+
/**
|
|
705
|
+
* The most releases one `workspace_release_bulk` may carry: one inventory page, so a controller
|
|
706
|
+
* can clean what it was just shown in one ask, and an answer of that size stays well under the
|
|
707
|
+
* frame cap.
|
|
708
|
+
*/
|
|
709
|
+
export declare const MAX_BULK_RELEASES = 25;
|
|
710
|
+
/**
|
|
711
|
+
* Release several worktrees in one ask. Each entry is judged and released on its own; one refusal
|
|
712
|
+
* never aborts the rest, and the answer carries one result per entry in the ask's order. A key
|
|
713
|
+
* named twice in one ask is refused on its second entry.
|
|
714
|
+
*/
|
|
715
|
+
export interface WorkspaceReleaseBulk {
|
|
716
|
+
readonly kind: 'workspace_release_bulk';
|
|
717
|
+
readonly requestId: string;
|
|
718
|
+
readonly releases: readonly WorkspaceReleaseEntry[];
|
|
719
|
+
}
|
|
720
|
+
export interface WorkspaceReleaseBulkResult {
|
|
721
|
+
readonly kind: 'workspace_release_bulk_result';
|
|
722
|
+
readonly requestId: string;
|
|
723
|
+
readonly results: readonly WorkspaceReleaseEntryResult[];
|
|
724
|
+
}
|
|
725
|
+
/** The two per-ask flags, both defaulting to the safe side. */
|
|
726
|
+
export interface WorkspaceReleaseFlags {
|
|
727
|
+
readonly deleteBranch?: boolean;
|
|
728
|
+
readonly force?: boolean;
|
|
729
|
+
}
|
|
730
|
+
/** Build a `workspace_release`. A string target is a key; `{ path }` addresses by path. */
|
|
731
|
+
export declare function workspaceRelease(requestId: string, target: string | {
|
|
732
|
+
readonly path: string;
|
|
733
|
+
}, flags?: WorkspaceReleaseFlags): WorkspaceRelease;
|
|
734
|
+
/** Build a `workspace_release_result`. Every omitted member takes the released-or-absent value. */
|
|
735
|
+
export declare function workspaceReleaseResult(requestId: string, outcome?: Partial<WorkspaceReleaseEntryResult>): WorkspaceReleaseResult;
|
|
736
|
+
/** Build one entry result with the released-or-absent defaults. */
|
|
737
|
+
export declare function workspaceReleaseEntryResult(outcome?: Partial<WorkspaceReleaseEntryResult>): WorkspaceReleaseEntryResult;
|
|
738
|
+
/** Build a `workspace_release_bulk`. */
|
|
739
|
+
export declare function workspaceReleaseBulk(requestId: string, releases: readonly WorkspaceReleaseEntry[]): WorkspaceReleaseBulk;
|
|
740
|
+
/** Build a `workspace_release_bulk_result`. */
|
|
741
|
+
export declare function workspaceReleaseBulkResult(requestId: string, results: readonly WorkspaceReleaseEntryResult[]): WorkspaceReleaseBulkResult;
|
|
742
|
+
/**
|
|
743
|
+
* The most entries one `host_configure` may carry: the closed config key set is smaller than this,
|
|
744
|
+
* so a legal ask never meets the bound and an ask past it is malformed rather than large.
|
|
745
|
+
*/
|
|
746
|
+
export declare const MAX_CONFIGURE_ENTRIES = 8;
|
|
747
|
+
/**
|
|
748
|
+
* One setting in a `host_configure` ask. `value: null` removes the key from the host's config
|
|
749
|
+
* file; the wire has no `undefined`, and a list of entries carries exactly what was asked, so no
|
|
750
|
+
* sentinel is needed for "not mentioned".
|
|
751
|
+
*/
|
|
752
|
+
export interface HostConfigureEntry {
|
|
753
|
+
readonly key: string;
|
|
754
|
+
readonly value: string | null;
|
|
755
|
+
}
|
|
756
|
+
/**
|
|
757
|
+
* Ask this host to change its own configuration. Host-scoped, like the discovery asks and the
|
|
758
|
+
* reap: the routing key is a channel the controller chose.
|
|
759
|
+
*
|
|
760
|
+
* The host validates every entry before it touches disk (the keys it takes over the wire, the
|
|
761
|
+
* shape of each value, the posture the whole set produces), writes its config file through the
|
|
762
|
+
* same module `periscope config` uses, and rebuilds what can be rebuilt live. A change to the
|
|
763
|
+
* workspace roots is refused while any session is live or opening: a session releases through
|
|
764
|
+
* the provider that provisioned it, and swapping roots under one turns that release into a guess.
|
|
765
|
+
* The environment still wins per key; a written value the environment shadows is reported as
|
|
766
|
+
* overridden rather than silently inert.
|
|
767
|
+
*/
|
|
768
|
+
export interface HostConfigure {
|
|
769
|
+
readonly kind: 'host_configure';
|
|
770
|
+
readonly requestId: string;
|
|
771
|
+
readonly entries: readonly HostConfigureEntry[];
|
|
772
|
+
}
|
|
773
|
+
/**
|
|
774
|
+
* The configure's answer, and every exit is this one kind.
|
|
775
|
+
*
|
|
776
|
+
* `configuration` is the EFFECTIVE view after the act, refused or not: on a refusal it is the
|
|
777
|
+
* unchanged view, so a controller always learns what the host runs with. `overriddenByEnvironment`
|
|
778
|
+
* names the wire-settable keys the host's environment sets, whose file values are unreachable.
|
|
779
|
+
*/
|
|
780
|
+
export interface HostConfigureResult {
|
|
781
|
+
readonly kind: 'host_configure_result';
|
|
782
|
+
readonly requestId: string;
|
|
783
|
+
readonly configuration: HostConfiguration;
|
|
784
|
+
readonly overriddenByEnvironment: readonly string[];
|
|
785
|
+
/** The keys written but not in effect until the next start: the two control-plane URLs. */
|
|
786
|
+
readonly pendingRestart: readonly string[];
|
|
787
|
+
/** Null = every entry applied. Otherwise the named reason nothing was written. */
|
|
788
|
+
readonly refusal: WireRefusal | null;
|
|
789
|
+
}
|
|
790
|
+
/**
|
|
791
|
+
* How many worktrees one `workspace_list_result` carries. A path is bounded at 1000 characters, a
|
|
792
|
+
* branch at 400 and a key at 200, so a full page stays under the frame cap with the same margin
|
|
793
|
+
* the transcript page keeps.
|
|
794
|
+
*/
|
|
795
|
+
export declare const WORKSPACE_PAGE_SIZE = 25;
|
|
796
|
+
/**
|
|
797
|
+
* How many entries one `transcript_list_result` carries.
|
|
798
|
+
*
|
|
799
|
+
* Bounded so the frame stays well under `MAX_FRAME_BYTES` (64 KiB): an entry is a slug (measured
|
|
800
|
+
* up to about 80 characters on one real 139-slug corpus), a 36-character session id and two
|
|
801
|
+
* integers, comfortably under 300 bytes of JSON each, so 100 entries is under half the cap even
|
|
802
|
+
* with slugs twice as long as any measured one.
|
|
803
|
+
*/
|
|
804
|
+
export declare const TRANSCRIPT_PAGE_SIZE = 100;
|
|
805
|
+
/**
|
|
806
|
+
* The bulk-lane locator namespace for a transcript read: `claude-transcript:{projectSlug}/{sessionId}`.
|
|
807
|
+
* A `bulk_request.what` that a host resolves to the agent CLI's own transcript files starts with
|
|
808
|
+
* this prefix; the slug is the CLI's flattened project directory name and is opaque.
|
|
809
|
+
*/
|
|
810
|
+
export declare const TRANSCRIPT_WHAT_PREFIX = "claude-transcript:";
|
|
811
|
+
/**
|
|
812
|
+
* One worktree under the host's workspace root, as git reports it. `key` is the directory's
|
|
813
|
+
* last segment, which is the workspace key the host provisioned it at, so a controller can address
|
|
814
|
+
* it by key; `path` is what the host's transitions carry as `where.worktree`, so a controller can
|
|
815
|
+
* address it by path as well. `merged` says whether the branch's tip is already in the
|
|
816
|
+
* repository's default branch, null when no default branch could be named.
|
|
817
|
+
*/
|
|
818
|
+
export interface WorkspaceListEntry {
|
|
819
|
+
readonly key: string;
|
|
820
|
+
readonly path: string;
|
|
821
|
+
readonly branch: string | null;
|
|
822
|
+
readonly head: string | null;
|
|
823
|
+
readonly detached: boolean;
|
|
824
|
+
readonly locked: boolean;
|
|
825
|
+
readonly prunable: boolean;
|
|
826
|
+
readonly merged: boolean | null;
|
|
827
|
+
/**
|
|
828
|
+
* How many commits the branch holds past the default branch (`rev-list --count <default>..<branch>`).
|
|
829
|
+
* Zero with `merged: true` is "nothing here yet", not "merged work"; null when detached or when no
|
|
830
|
+
* default branch could be named.
|
|
831
|
+
*/
|
|
832
|
+
readonly aheadCount: number | null;
|
|
833
|
+
readonly lastCommitAt: string | null;
|
|
834
|
+
}
|
|
835
|
+
/**
|
|
836
|
+
* Ask this host what worktrees exist under its workspace root, from disk. Host-scoped and paged
|
|
837
|
+
* like `transcript_list`. Only what the host provisioned is listed: the repository itself and any
|
|
838
|
+
* checkout outside the workspace root never appear, so no cleanup a controller composes from this
|
|
839
|
+
* answer can name them.
|
|
840
|
+
*/
|
|
841
|
+
export interface WorkspaceList {
|
|
842
|
+
readonly kind: 'workspace_list';
|
|
843
|
+
readonly requestId: string;
|
|
844
|
+
readonly fromIndex: number;
|
|
845
|
+
}
|
|
846
|
+
/** The inventory's answer; every exit is this one kind. `refusal` non-null = the host could not look. */
|
|
847
|
+
export interface WorkspaceListResult {
|
|
848
|
+
readonly kind: 'workspace_list_result';
|
|
849
|
+
readonly requestId: string;
|
|
850
|
+
readonly entries: readonly WorkspaceListEntry[];
|
|
851
|
+
readonly totalCount: number;
|
|
852
|
+
readonly nextIndex: number | null;
|
|
853
|
+
readonly defaultBranch: string | null;
|
|
854
|
+
readonly refusal: WireRefusal | null;
|
|
855
|
+
}
|
|
856
|
+
/** Build a `workspace_list`. The only supported way to put one on the wire. */
|
|
857
|
+
export declare function workspaceList(requestId: string, fromIndex?: number): WorkspaceList;
|
|
858
|
+
/** Build a `workspace_list_result`. Fills `refusal` with null, the answered page. */
|
|
859
|
+
export declare function workspaceListResult(requestId: string, entries: readonly WorkspaceListEntry[], page: {
|
|
860
|
+
readonly totalCount: number;
|
|
861
|
+
readonly nextIndex?: number;
|
|
862
|
+
readonly defaultBranch?: string | null;
|
|
863
|
+
}, refusal?: WireRefusal): WorkspaceListResult;
|
|
864
|
+
/** Build a `host_configure`. The only supported way to put one on the wire. */
|
|
865
|
+
export declare function hostConfigure(requestId: string, entries: readonly HostConfigureEntry[]): HostConfigure;
|
|
866
|
+
/** Build a `host_configure_result`. Fills `refusal` with null, the applied answer. */
|
|
867
|
+
export declare function hostConfigureResult(requestId: string, configuration: HostConfiguration, overriddenByEnvironment: readonly string[], refusal?: WireRefusal, pendingRestart?: readonly string[]): HostConfigureResult;
|
|
868
|
+
/** Build a `session_list`. The only supported way to put one on the wire. */
|
|
869
|
+
export declare function sessionList(requestId: string): SessionList;
|
|
870
|
+
/** Build a `session_list_result`. */
|
|
871
|
+
export declare function sessionListResult(requestId: string, sessions: readonly SessionListEntry[], counts: {
|
|
872
|
+
liveCount: number;
|
|
873
|
+
provisioningCount: number;
|
|
874
|
+
}): SessionListResult;
|
|
875
|
+
/** Build a `transcript_list`. `fromIndex` defaults to the start. */
|
|
876
|
+
export declare function transcriptList(requestId: string, fromIndex?: number): TranscriptList;
|
|
877
|
+
/** Build a `transcript_list_result`. Fills `nextIndex` with null so absence is stated, not deleted. */
|
|
878
|
+
export declare function transcriptListResult(requestId: string, entries: readonly TranscriptListEntry[], totals: {
|
|
879
|
+
totalCount: number;
|
|
880
|
+
nextIndex?: number;
|
|
881
|
+
}): TranscriptListResult;
|
|
882
|
+
/** Build a `transcript_tail`. Fills `needle` with null, meaning "any user-text entry". */
|
|
883
|
+
export declare function transcriptTail(requestId: string, projectSlug: string, sessionId: string, fromOffset: number, needle?: string): TranscriptTail;
|
|
884
|
+
/** Build a `transcript_tail_result`. Fills the stat pair with null so absence is stated. */
|
|
885
|
+
export declare function transcriptTailResult(requestId: string, answer: {
|
|
886
|
+
found: boolean;
|
|
887
|
+
absent: boolean;
|
|
888
|
+
newOffset: number;
|
|
889
|
+
sizeBytes?: number;
|
|
890
|
+
mtimeMs?: number;
|
|
891
|
+
}): TranscriptTailResult;
|
|
892
|
+
/** Build a `transcript_failed`. */
|
|
893
|
+
export declare function transcriptFailed(requestId: string, refusal: WireRefusal): TranscriptFailed;
|
|
894
|
+
/**
|
|
895
|
+
* How many entries one `repository_list_result` carries. A directory with more than this many
|
|
896
|
+
* children is listed to the cap and marked `truncated`; names are bounded by the filesystem, so a
|
|
897
|
+
* full page stays well under the frame cap.
|
|
898
|
+
*/
|
|
899
|
+
export declare const MAX_REPOSITORY_ENTRIES = 500;
|
|
900
|
+
/**
|
|
901
|
+
* The most text one `repository_read_result` carries. The frame cap is 64 KiB and the envelope plus
|
|
902
|
+
* JSON escaping must fit inside it, so the text is bounded below the cap with margin; a longer file
|
|
903
|
+
* is answered to this many bytes and marked `truncated`.
|
|
904
|
+
*/
|
|
905
|
+
export declare const MAX_REPOSITORY_READ_BYTES: number;
|
|
906
|
+
/**
|
|
907
|
+
* Ask this host to list one directory of its repository, by a path relative to the repository root
|
|
908
|
+
* (`''` is the root itself). Host-scoped like `transcript_list`. The host answers names only and
|
|
909
|
+
* never leaves the root: a path that resolves outside it is refused by name, so a controller can
|
|
910
|
+
* show a checkout's shape and pick a file to read without the host ever serving another directory.
|
|
911
|
+
*/
|
|
912
|
+
export interface RepositoryList {
|
|
913
|
+
readonly kind: 'repository_list';
|
|
914
|
+
readonly requestId: string;
|
|
915
|
+
readonly path: string;
|
|
916
|
+
}
|
|
917
|
+
/** One child of a listed directory: its name, whether it is a directory, and its stat pair. */
|
|
918
|
+
export interface RepositoryEntry {
|
|
919
|
+
readonly name: string;
|
|
920
|
+
readonly directory: boolean;
|
|
921
|
+
readonly sizeBytes: number;
|
|
922
|
+
readonly mtimeMs: number;
|
|
923
|
+
}
|
|
924
|
+
/** The listing's answer; every exit is this one kind. `refusal` non-null = nothing was listed. */
|
|
925
|
+
export interface RepositoryListResult {
|
|
926
|
+
readonly kind: 'repository_list_result';
|
|
927
|
+
readonly requestId: string;
|
|
928
|
+
/** Sorted by name, at most `MAX_REPOSITORY_ENTRIES`. */
|
|
929
|
+
readonly entries: readonly RepositoryEntry[];
|
|
930
|
+
/** True when the directory held more than the cap. */
|
|
931
|
+
readonly truncated: boolean;
|
|
932
|
+
readonly refusal: WireRefusal | null;
|
|
933
|
+
}
|
|
934
|
+
/**
|
|
935
|
+
* Ask this host for the text of one file of its repository, by a path relative to the repository
|
|
936
|
+
* root, at most `maxBytes` of it (1 to `MAX_REPOSITORY_READ_BYTES`). Text files only: a file with a
|
|
937
|
+
* NUL byte in its head is refused rather than served, because the answer is a string.
|
|
938
|
+
*/
|
|
939
|
+
export interface RepositoryRead {
|
|
940
|
+
readonly kind: 'repository_read';
|
|
941
|
+
readonly requestId: string;
|
|
942
|
+
readonly path: string;
|
|
943
|
+
readonly maxBytes: number;
|
|
944
|
+
}
|
|
945
|
+
/** The read's answer; every exit is this one kind. `text` is null exactly when `refusal` is not. */
|
|
946
|
+
export interface RepositoryReadResult {
|
|
947
|
+
readonly kind: 'repository_read_result';
|
|
948
|
+
readonly requestId: string;
|
|
949
|
+
/** UTF-8 text, the first `maxBytes` of the file at most. */
|
|
950
|
+
readonly text: string | null;
|
|
951
|
+
/** The file's whole size, so a caller can see how much `truncated` left behind. */
|
|
952
|
+
readonly sizeBytes: number;
|
|
953
|
+
readonly truncated: boolean;
|
|
954
|
+
readonly refusal: WireRefusal | null;
|
|
955
|
+
}
|
|
956
|
+
/** Build a `repository_list`. The only supported way to put one on the wire. */
|
|
957
|
+
export declare function repositoryList(requestId: string, path?: string): RepositoryList;
|
|
958
|
+
/** Build a `repository_list_result`. Fills `refusal` with null, the listed answer. */
|
|
959
|
+
export declare function repositoryListResult(requestId: string, entries: readonly RepositoryEntry[], truncated?: boolean, refusal?: WireRefusal): RepositoryListResult;
|
|
960
|
+
/** Build a `repository_read`. Fills `maxBytes` with the cap. */
|
|
961
|
+
export declare function repositoryRead(requestId: string, path: string, maxBytes?: number): RepositoryRead;
|
|
962
|
+
/** Build a `repository_read_result`. Fills `refusal` with null, the read answer. */
|
|
963
|
+
export declare function repositoryReadResult(requestId: string, read: {
|
|
964
|
+
readonly text: string | null;
|
|
965
|
+
readonly sizeBytes: number;
|
|
966
|
+
readonly truncated: boolean;
|
|
967
|
+
}, refusal?: WireRefusal): RepositoryReadResult;
|
|
968
|
+
/**
|
|
969
|
+
* A refusal as it arrives from the wire: `reason` is a plain string, not the closed enum.
|
|
970
|
+
*
|
|
971
|
+
* Strict out, tolerant in, and the asymmetry is deliberate. Nothing this host writes can
|
|
972
|
+
* carry an undeclared reason: `encode` validates it on the way out, so the guarantee is enforced at
|
|
973
|
+
* the one place a frame becomes bytes rather than resting on a type a cast defeats. But a frame
|
|
974
|
+
* arriving with a reason this build has never seen is a newer peer, not a corrupt frame, and
|
|
975
|
+
* rejecting it would destroy a delivery receipt (the outcome, the delivery id, the detail) over a
|
|
976
|
+
* word. `codec.ts`'s own header already promises "unknown fields survive… an older host relaying a
|
|
977
|
+
* frame does not quietly destroy information it did not understand"; a closed enum value is that
|
|
978
|
+
* same information one level in.
|
|
979
|
+
*
|
|
980
|
+
* Read it with `readRefusal`. Same shape and same argument as `SessionUpdate.body` above: a
|
|
981
|
+
* producer cannot build a malformed one, a consumer cannot read one without checking, and the wire
|
|
982
|
+
* stays tolerant.
|
|
983
|
+
*/
|
|
984
|
+
export interface WireRefusal {
|
|
985
|
+
/** A `RefusalReason` when this build knows it; any string when the peer is newer. */
|
|
986
|
+
readonly reason: string;
|
|
987
|
+
readonly detail: string;
|
|
988
|
+
}
|
|
989
|
+
/**
|
|
990
|
+
* A refusal read off the wire: recognised and narrowed, or unrecognised with the raw value kept.
|
|
991
|
+
*
|
|
992
|
+
* The raw string travels. An unrecognised reason is never mapped onto a known one and never
|
|
993
|
+
* dropped: a consumer that discards what it did not understand makes a newer peer's failure
|
|
994
|
+
* invisible on the only side that could have reported it. This is the gate's unknown-decision rule
|
|
995
|
+
* in the transport: an unrecognised value is a named outcome, not a silent conversion.
|
|
996
|
+
*/
|
|
997
|
+
export type ReadRefusal = {
|
|
998
|
+
readonly recognised: true;
|
|
999
|
+
readonly refusal: Refusal;
|
|
1000
|
+
} | {
|
|
1001
|
+
readonly recognised: false;
|
|
1002
|
+
readonly raw: string;
|
|
1003
|
+
readonly detail: string;
|
|
1004
|
+
};
|
|
1005
|
+
export declare function readRefusal(wire: WireRefusal): ReadRefusal;
|
|
1006
|
+
export type SessionPayload = SessionUpdate | SessionDelta | SessionNew | SessionPrompt | SessionCancel | SessionConfigure | BulkRequest | BulkDelivered | BulkFailed | SessionList | SessionListResult | TranscriptList | TranscriptListResult | TranscriptTail | TranscriptTailResult | TranscriptFailed | WorkspaceRelease | WorkspaceReleaseResult | WorkspaceReleaseBulk | WorkspaceReleaseBulkResult | HostConfigure | HostConfigureResult | WorkspaceList | WorkspaceListResult | RepositoryList | RepositoryListResult | RepositoryRead | RepositoryReadResult;
|
|
1007
|
+
export type SessionPayloadKind = SessionPayload['kind'];
|
|
1008
|
+
/**
|
|
1009
|
+
* What may be dropped when the offline queue is full, stated as data so the decision lives in one
|
|
1010
|
+
* place instead of inside a queue method. Everything absent from this list is a transition or a
|
|
1011
|
+
* receipt, and losing one of those is a lie about what happened.
|
|
1012
|
+
*/
|
|
1013
|
+
export declare const DROPPABLE_KINDS: readonly SessionPayloadKind[];
|
|
1014
|
+
export declare function isDroppable(kind: SessionPayloadKind): boolean;
|
|
1015
|
+
/**
|
|
1016
|
+
* The longest value `HostConfiguration` carries. Paths and URLs are host-controlled text on a
|
|
1017
|
+
* frame the controller must accept before anything else, so each is bounded here rather than
|
|
1018
|
+
* trusted; a controller stores them under the same cap.
|
|
1019
|
+
*/
|
|
1020
|
+
export declare const MAX_CONFIGURATION_VALUE_LENGTH = 1000;
|
|
1021
|
+
/**
|
|
1022
|
+
* How this host is configured, as values: the read half of `periscope config`.
|
|
1023
|
+
*
|
|
1024
|
+
* The `workspace:*` capability markers say which mode a host is in; these say what it is pointed
|
|
1025
|
+
* at. Every member is `T | null` (absent is null, never undefined), and null means the setting is
|
|
1026
|
+
* not set on the host, not that the host declined to say. `controllerUrl` is what the host dialled
|
|
1027
|
+
* to deliver this frame, reported so a controller can show it beside the rest; nothing here is an
|
|
1028
|
+
* ask, so nothing here can be refused.
|
|
1029
|
+
*/
|
|
1030
|
+
export interface HostConfiguration {
|
|
1031
|
+
readonly repositoryRoot: string | null;
|
|
1032
|
+
readonly workspaceRoot: string | null;
|
|
1033
|
+
readonly branchScheme: string | null;
|
|
1034
|
+
/** Where the agent keeps its transcripts: derived from `agentHome`. Reported, never set on its own. */
|
|
1035
|
+
readonly transcriptsRoot: string | null;
|
|
1036
|
+
/**
|
|
1037
|
+
* The controller link URL the host's configuration names. Settable over the link; the value
|
|
1038
|
+
* in effect is the one dialled at start, so a change rides `pendingRestart` until the next start.
|
|
1039
|
+
*/
|
|
1040
|
+
readonly controllerUrl: string | null;
|
|
1041
|
+
/** The decision endpoint the host's configuration names. Settable over the link, same rule. */
|
|
1042
|
+
readonly decisionUrl: string | null;
|
|
1043
|
+
/** Where the agent CLI keeps its state. Settable over the link; the transcripts root derives from it. */
|
|
1044
|
+
readonly agentHome: string | null;
|
|
1045
|
+
}
|
|
1046
|
+
/** A `HostConfiguration` with nothing set: what a host composed without one reports. */
|
|
1047
|
+
export declare function unsetHostConfiguration(): HostConfiguration;
|
|
1048
|
+
/** The host's opening frame. Capability negotiation at hello, borrowed from ACP. */
|
|
1049
|
+
export interface LinkHello {
|
|
1050
|
+
readonly kind: 'link_hello';
|
|
1051
|
+
readonly protocolVersion: number;
|
|
1052
|
+
readonly hostId: string;
|
|
1053
|
+
readonly capabilities: readonly string[];
|
|
1054
|
+
/** What the host already holds per session, so the controller knows what can be replayed. */
|
|
1055
|
+
readonly cursors: readonly SessionCursor[];
|
|
1056
|
+
/** The values this host runs with. Reported, never negotiated. */
|
|
1057
|
+
readonly configuration: HostConfiguration;
|
|
1058
|
+
/**
|
|
1059
|
+
* The keys whose configured value is not the one in effect: written to the file, dialled
|
|
1060
|
+
* only at the next start. Empty on a host that runs what its file says.
|
|
1061
|
+
*/
|
|
1062
|
+
readonly pendingRestart: readonly string[];
|
|
1063
|
+
/**
|
|
1064
|
+
* The versions this host speaks. `protocolVersion` stays the newest of them; the controller
|
|
1065
|
+
* answers with its choice inside the overlap of the two windows.
|
|
1066
|
+
*/
|
|
1067
|
+
readonly protocolRange: ProtocolRange;
|
|
1068
|
+
}
|
|
1069
|
+
/**
|
|
1070
|
+
* The controller's answer. `protocolVersion` is the version the controller chose inside the overlap
|
|
1071
|
+
* of the two windows; the host accepts any version in its own window and refuses the rest, naming
|
|
1072
|
+
* both windows.
|
|
1073
|
+
*/
|
|
1074
|
+
export interface LinkWelcome {
|
|
1075
|
+
readonly kind: 'link_welcome';
|
|
1076
|
+
readonly protocolVersion: number;
|
|
1077
|
+
readonly capabilities: readonly string[];
|
|
1078
|
+
/** The last `seq` the controller received per session — the host replays past these. */
|
|
1079
|
+
readonly cursors: readonly SessionCursor[];
|
|
1080
|
+
}
|
|
1081
|
+
export interface SessionCursor {
|
|
1082
|
+
readonly sessionId: string;
|
|
1083
|
+
readonly seq: number;
|
|
1084
|
+
}
|
|
1085
|
+
/**
|
|
1086
|
+
* What the controller has durably received, per session.
|
|
1087
|
+
*
|
|
1088
|
+
* Without this the host cannot know when a frame is safe to forget, so it would either discard on
|
|
1089
|
+
* write — losing anything in flight when a socket dies — or retain forever. The ack is what makes
|
|
1090
|
+
* the retention window finite AND replay complete.
|
|
1091
|
+
*/
|
|
1092
|
+
export interface LinkAck {
|
|
1093
|
+
readonly kind: 'link_ack';
|
|
1094
|
+
readonly cursors: readonly SessionCursor[];
|
|
1095
|
+
}
|
|
1096
|
+
/** Heartbeat, both directions. A half-open socket answers neither. */
|
|
1097
|
+
export interface LinkPing {
|
|
1098
|
+
readonly kind: 'link_ping';
|
|
1099
|
+
readonly nonce: string;
|
|
1100
|
+
}
|
|
1101
|
+
export interface LinkPong {
|
|
1102
|
+
readonly kind: 'link_pong';
|
|
1103
|
+
readonly nonce: string;
|
|
1104
|
+
}
|
|
1105
|
+
/** A deliberate close, carrying why. A silent disconnect is the thing this exists to distinguish. */
|
|
1106
|
+
export interface LinkBye {
|
|
1107
|
+
readonly kind: 'link_bye';
|
|
1108
|
+
readonly cause: string;
|
|
1109
|
+
}
|
|
1110
|
+
export type ControlPayload = LinkHello | LinkWelcome | LinkAck | LinkPing | LinkPong | LinkBye;
|
|
1111
|
+
export type ControlPayloadKind = ControlPayload['kind'];
|
|
1112
|
+
/**
|
|
1113
|
+
* A sequenced, replayable frame belonging to one session.
|
|
1114
|
+
*
|
|
1115
|
+
* `seq` is monotonic per session per direction, from 1, dense (+1 per frame). Dense is what makes
|
|
1116
|
+
* a gap detectable by arithmetic alone: the receiver's expected next is always `last + 1`.
|
|
1117
|
+
*
|
|
1118
|
+
* A seq is minted only when its frame is first written to the wire. Anything refused or dropped
|
|
1119
|
+
* before that moment never had one, so designed loss (an oversized payload, a discard or a
|
|
1120
|
+
* displacement under pressure) cannot put a hole in the numbering. That is what lets "dense" be
|
|
1121
|
+
* a construction rather than a hope.
|
|
1122
|
+
*
|
|
1123
|
+
* ---------------------------------------------------------------------------------------------
|
|
1124
|
+
* There are three session ids in this package and they are not interchangeable. Conflating any
|
|
1125
|
+
* pair produces a trace that lies, so all three are named here, once:
|
|
1126
|
+
*
|
|
1127
|
+
* `SessionFrame.sessionId` The routing and ordering key. The controller chooses it, and
|
|
1128
|
+
* it must, because `session_new` addresses a session that does
|
|
1129
|
+
* not exist yet. It is what `seq` is dense per, and it is
|
|
1130
|
+
* stable for the session's whole life. The host maps and orders
|
|
1131
|
+
* by it; interpreting it is this field's job.
|
|
1132
|
+
*
|
|
1133
|
+
* `SessionTransition.sessionId` The agent's own id, and a fact rather than a key. The agent
|
|
1134
|
+
* mints it at `system/init`; it is `null` before that and never
|
|
1135
|
+
* null again. The host carries it and never invents one.
|
|
1136
|
+
*
|
|
1137
|
+
* `SessionTransition.correlationId` The controller's meaning handle, and opaque. Never
|
|
1138
|
+
* interpreted here; see its own comment in state/model.ts.
|
|
1139
|
+
*
|
|
1140
|
+
* A controller will very likely pass the same string as the routing key and the correlation id.
|
|
1141
|
+
* That is fine and expected. The host must still never assume it, derive one from the other, or
|
|
1142
|
+
* fall back from one to the other: they differ in contract even when they coincide in value. One
|
|
1143
|
+
* is interpreted by construction, the other is forbidden to be.
|
|
1144
|
+
*
|
|
1145
|
+
* The consequence that makes this load-bearing rather than tidy: a session's first transitions are
|
|
1146
|
+
* recorded before the agent has named itself (`UserPromptSubmit` fires ahead of `system/init`), and
|
|
1147
|
+
* a session that dies in start-up never names itself at all. Keying frames by the agent's id would
|
|
1148
|
+
* make exactly those frames unsendable, and they are the ones that explain the failure.
|
|
1149
|
+
* ---------------------------------------------------------------------------------------------
|
|
1150
|
+
*/
|
|
1151
|
+
export interface SessionFrame {
|
|
1152
|
+
readonly frame: 'session';
|
|
1153
|
+
readonly sessionId: string;
|
|
1154
|
+
readonly seq: number;
|
|
1155
|
+
readonly at: string;
|
|
1156
|
+
readonly payload: SessionPayload;
|
|
1157
|
+
}
|
|
1158
|
+
/** An unsequenced frame about the link itself. */
|
|
1159
|
+
export interface ControlFrame {
|
|
1160
|
+
readonly frame: 'control';
|
|
1161
|
+
readonly at: string;
|
|
1162
|
+
readonly payload: ControlPayload;
|
|
1163
|
+
}
|
|
1164
|
+
export type Frame = SessionFrame | ControlFrame;
|
|
1165
|
+
export declare function isSessionFrame(frame: Frame): frame is SessionFrame;
|
|
1166
|
+
export declare function isControlFrame(frame: Frame): frame is ControlFrame;
|
|
1167
|
+
/**
|
|
1168
|
+
* A frame's identity, and the reason `seq` is shaped this way: this string is exactly what an SSE
|
|
1169
|
+
* `Last-Event-ID` header carries. Moving to one stream per session later needs no change to the
|
|
1170
|
+
* frame; the cursor set decomposes into per-stream ids one for one.
|
|
1171
|
+
*/
|
|
1172
|
+
export declare function frameId(frame: SessionFrame): string;
|
|
1173
|
+
/** JSON, as a type. Bodies are parsed but not interpreted here. */
|
|
1174
|
+
export type JsonValue = string | number | boolean | null | JsonValue[] | {
|
|
1175
|
+
[key: string]: JsonValue;
|
|
1176
|
+
};
|
|
1177
|
+
export type JsonObject = {
|
|
1178
|
+
[key: string]: JsonValue;
|
|
1179
|
+
};
|
|
1180
|
+
//# sourceMappingURL=frames.d.ts.map
|