@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,417 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The declared state model — what a session is at a moment, and what caused it to become that.
|
|
3
|
+
*
|
|
4
|
+
* This is the one model. There is no second status word anywhere in this package: a layer that
|
|
5
|
+
* finds itself wanting one has found a state missing from here, not a column of its own. A single
|
|
6
|
+
* word such as `running` that covers responding, tool-calling, waiting on a permission decision,
|
|
7
|
+
* compacting, and finished-and-waiting-for-input answers nobody's question, and every reader
|
|
8
|
+
* downstream of it ends up inferring.
|
|
9
|
+
*
|
|
10
|
+
* Two fields, never one composed string, and structured all the way down.
|
|
11
|
+
* `state` is the coarse lifecycle; `activity` is what a working session is blocked on right now. A
|
|
12
|
+
* composed string cannot be queried or aggregated without parsing it back apart, and "show me every
|
|
13
|
+
* session waiting on a permission decision" is the question this model is built to answer. The same
|
|
14
|
+
* argument applies one level in, which is why `activity` is `{kind, name}` rather than `tool:Bash`:
|
|
15
|
+
* `formatActivity` exists for display, and the structure is the truth.
|
|
16
|
+
*
|
|
17
|
+
* The vocabulary is the SDK's wherever the SDK has a word. `requesting` and `compacting` are
|
|
18
|
+
* `SDKStatus` verbatim; `permission`, `tool`, `subagent` are the SDK's nouns. Where the SDK is
|
|
19
|
+
* silent this package is the author, and the table below says which is which — the same separation
|
|
20
|
+
* `core/vocab.ts` draws between SDK_NOUNS and HOST_NOUNS, for the same reason.
|
|
21
|
+
*
|
|
22
|
+
* Nothing here imports the SDK or a runtime, so the model ships on `periscope/protocol` and a
|
|
23
|
+
* controller can read a transition without acquiring a package that can spawn a process.
|
|
24
|
+
*/
|
|
25
|
+
import type { RefusalReason } from '../core/refusal.js';
|
|
26
|
+
import { isRefusalReason } from '../core/refusal.js';
|
|
27
|
+
|
|
28
|
+
// ---------------------------------------------------------------------------
|
|
29
|
+
// state — the coarse lifecycle
|
|
30
|
+
// ---------------------------------------------------------------------------
|
|
31
|
+
|
|
32
|
+
export const SESSION_STATES = [
|
|
33
|
+
/** The process is up; the agent has not reported itself. It has no id yet. */
|
|
34
|
+
'spawning',
|
|
35
|
+
/**
|
|
36
|
+
* The agent reported itself: id, model, tool inventory, version receipt all known.
|
|
37
|
+
*
|
|
38
|
+
* It arrives mid-turn, and the trace goes `working -> ready -> working`. That reads like a
|
|
39
|
+
* step backwards and it is not: it is literally what happens. Observed on a real session:
|
|
40
|
+
*
|
|
41
|
+
* 1 spawning -> spawning control/create_requested
|
|
42
|
+
* 2 spawning -> spawning control/prompt_submitted
|
|
43
|
+
* 3 spawning -> working hook/UserPromptSubmit
|
|
44
|
+
* 4 working -> ready sdk-message/system/init <- here
|
|
45
|
+
* 5 ready -> working hook/PreToolUse
|
|
46
|
+
*
|
|
47
|
+
* The agent emits nothing at all until a turn is queued, and `UserPromptSubmit` fires ahead of
|
|
48
|
+
* `init`, so the session is genuinely already working when it says what it is. Ordering this
|
|
49
|
+
* state earlier would mean recording it before the event that causes it.
|
|
50
|
+
*
|
|
51
|
+
* It is kept rather than collapsed for two reasons. It is the only row where the session's
|
|
52
|
+
* identity becomes known — collapse it and "what is this session" needs the transcript. And a
|
|
53
|
+
* session created but never prompted never reaches it: it sits in `spawning` until the start
|
|
54
|
+
* timeout, so the state being missing is what makes that failure legible instead of a hang.
|
|
55
|
+
*/
|
|
56
|
+
'ready',
|
|
57
|
+
/** A turn is in flight. `activity` says what it is blocked on. */
|
|
58
|
+
'working',
|
|
59
|
+
/** A turn ended cleanly and the session is waiting for input. */
|
|
60
|
+
'idle',
|
|
61
|
+
/** A turn ended abnormally. The cause names which way. */
|
|
62
|
+
'errored',
|
|
63
|
+
/** An interrupt landed mid-turn. Distinct from `idle`: nobody chose to stop here. */
|
|
64
|
+
'interrupted',
|
|
65
|
+
/** Over. The cause says why, and it is never inferred from silence. */
|
|
66
|
+
'ended',
|
|
67
|
+
] as const;
|
|
68
|
+
|
|
69
|
+
export type SessionState = (typeof SESSION_STATES)[number];
|
|
70
|
+
|
|
71
|
+
// ---------------------------------------------------------------------------
|
|
72
|
+
// activity — what a working session is blocked on in the foreground
|
|
73
|
+
// ---------------------------------------------------------------------------
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Where each activity kind's name comes from. Two are the SDK's own words; four extend it.
|
|
77
|
+
*
|
|
78
|
+
* `requesting` and `compacting` are `SDKStatus` verbatim — two values and a null. The SDK has no
|
|
79
|
+
* word for the other four situations, so this package names them, and this table is where that is
|
|
80
|
+
* recorded rather than left for a reader to guess.
|
|
81
|
+
*/
|
|
82
|
+
export const ACTIVITY_KINDS = [
|
|
83
|
+
/** SDK: `SDKStatus`. A model request is in flight. */
|
|
84
|
+
'requesting',
|
|
85
|
+
/** SDK: `SDKStatus`. Context compaction is running. */
|
|
86
|
+
'compacting',
|
|
87
|
+
/** This package's own. A tool call is in flight — `name` is the SDK's `tool_name`. */
|
|
88
|
+
'tool',
|
|
89
|
+
/** This package's own. A subagent is running — `name` is the SDK's `agent_type`. */
|
|
90
|
+
'subagent',
|
|
91
|
+
/** This package's own. A permission decision is outstanding — `name` is the tool it is about. */
|
|
92
|
+
'permission',
|
|
93
|
+
/** This package's own. An MCP server is asking for input — `name` is `mcp_server_name`. */
|
|
94
|
+
'elicitation',
|
|
95
|
+
] as const;
|
|
96
|
+
|
|
97
|
+
export type ActivityKind = (typeof ACTIVITY_KINDS)[number];
|
|
98
|
+
|
|
99
|
+
/** Which activity kinds this package named itself, kept visible the way HOST_NOUNS is. */
|
|
100
|
+
export const HOST_ACTIVITY_KINDS: readonly ActivityKind[] = ['tool', 'subagent', 'permission', 'elicitation'];
|
|
101
|
+
|
|
102
|
+
/** SDK: `SDKStatus`'s two non-null values, adopted verbatim. */
|
|
103
|
+
export const SDK_ACTIVITY_KINDS: readonly ActivityKind[] = ['requesting', 'compacting'];
|
|
104
|
+
|
|
105
|
+
export interface SessionActivity {
|
|
106
|
+
readonly kind: ActivityKind;
|
|
107
|
+
/** The tool, subagent type or server this is about. Null where the kind carries no name. */
|
|
108
|
+
readonly name: string | null;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/** The display form — `tool:Bash`, `requesting`. For logs and humans; never parsed back. */
|
|
112
|
+
export function formatActivity(activity: SessionActivity | null): string {
|
|
113
|
+
if (activity === null) return 'none';
|
|
114
|
+
return activity.name === null ? activity.kind : `${activity.kind}:${activity.name}`;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export function sameActivity(left: SessionActivity | null, right: SessionActivity | null): boolean {
|
|
118
|
+
if (left === null || right === null) return left === right;
|
|
119
|
+
return left.kind === right.kind && left.name === right.name;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// ---------------------------------------------------------------------------
|
|
123
|
+
// cause — required, and closed at compile time
|
|
124
|
+
// ---------------------------------------------------------------------------
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Where a transition came from. A transition that cannot name its cause is not recorded.
|
|
128
|
+
*
|
|
129
|
+
* `refusal` is the sixth and it is load-bearing: a permission denial is a hook event, but a
|
|
130
|
+
* permission-path outage has no hook at all, and with five kinds the difference would have to ride
|
|
131
|
+
* `detail` — free text a reader greps instead of a fact code branches on. Conflating an outage with
|
|
132
|
+
* a deliberate "no" is the most expensive confusion a controller can make, so the two differ in
|
|
133
|
+
* `kind` and in `event`, twice over, neither of them prose.
|
|
134
|
+
*/
|
|
135
|
+
export const CAUSE_KINDS = ['hook', 'sdk-message', 'control', 'timeout', 'process', 'refusal'] as const;
|
|
136
|
+
|
|
137
|
+
export type CauseKind = (typeof CAUSE_KINDS)[number];
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* The SDK's hook events, mirrored as values so they can be validated at runtime.
|
|
141
|
+
*
|
|
142
|
+
* Kept in step by two checks, because a list copied out of a type rots silently: `coverage.ts`
|
|
143
|
+
* declares its table `satisfies Record<HookEvent, …>` so a new SDK event breaks the build, and
|
|
144
|
+
* `pins/hook-coverage.test.ts` parses the union straight out of the shipped `sdk.d.ts` and fails
|
|
145
|
+
* when this list disagrees with it.
|
|
146
|
+
*/
|
|
147
|
+
export const HOOK_EVENTS = [
|
|
148
|
+
'PreToolUse',
|
|
149
|
+
'PostToolUse',
|
|
150
|
+
'PostToolUseFailure',
|
|
151
|
+
'PostToolBatch',
|
|
152
|
+
'Notification',
|
|
153
|
+
'UserPromptSubmit',
|
|
154
|
+
'UserPromptExpansion',
|
|
155
|
+
'SessionStart',
|
|
156
|
+
'SessionEnd',
|
|
157
|
+
'Stop',
|
|
158
|
+
'StopFailure',
|
|
159
|
+
'SubagentStart',
|
|
160
|
+
'SubagentStop',
|
|
161
|
+
'PreCompact',
|
|
162
|
+
'PostCompact',
|
|
163
|
+
'PermissionRequest',
|
|
164
|
+
'PermissionDenied',
|
|
165
|
+
'Setup',
|
|
166
|
+
'TeammateIdle',
|
|
167
|
+
'TaskCreated',
|
|
168
|
+
'TaskCompleted',
|
|
169
|
+
'Elicitation',
|
|
170
|
+
'ElicitationResult',
|
|
171
|
+
'ConfigChange',
|
|
172
|
+
'WorktreeCreate',
|
|
173
|
+
'WorktreeRemove',
|
|
174
|
+
'InstructionsLoaded',
|
|
175
|
+
'CwdChanged',
|
|
176
|
+
'FileChanged',
|
|
177
|
+
'DirectoryAdded',
|
|
178
|
+
'MessageDisplay',
|
|
179
|
+
] as const;
|
|
180
|
+
|
|
181
|
+
export type HookEventName = (typeof HOOK_EVENTS)[number];
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* The SDK message discriminators this model names as causes — `type` for the plain kinds,
|
|
185
|
+
* `type/subtype` for the `system` family. Only the ones that can move a session appear here;
|
|
186
|
+
* `coverage.ts` accounts for all 39 members of the union, wired or declined.
|
|
187
|
+
*/
|
|
188
|
+
export const MESSAGE_EVENTS = [
|
|
189
|
+
'system/init',
|
|
190
|
+
'system/status',
|
|
191
|
+
'system/session_state_changed',
|
|
192
|
+
'system/compact_boundary',
|
|
193
|
+
'system/task_started',
|
|
194
|
+
'system/task_updated',
|
|
195
|
+
'system/task_notification',
|
|
196
|
+
'system/permission_denied',
|
|
197
|
+
'system/elicitation_complete',
|
|
198
|
+
'system/worker_shutting_down',
|
|
199
|
+
'assistant',
|
|
200
|
+
'result',
|
|
201
|
+
] as const;
|
|
202
|
+
|
|
203
|
+
export type MessageEventName = (typeof MESSAGE_EVENTS)[number];
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Calls the host makes on itself or is asked to make. Not SDK events — decisions.
|
|
207
|
+
*
|
|
208
|
+
* `kind` and `event` are validated independently and never as a pair. `machine.ts`'s `nameable()`
|
|
209
|
+
* checks that the kind is a declared kind and that the event is a declared event, in two separate
|
|
210
|
+
* guards — so `{kind:'hook', event:'permission_denied'}` passes while being incoherent. Pairing a
|
|
211
|
+
* cause with the right kind is the author's job, not the machine's.
|
|
212
|
+
*
|
|
213
|
+
* `permission_denied` sits here rather than in HOOK_EVENTS because a gate's deny is a call the host
|
|
214
|
+
* makes on itself: it is decided by this package, not reported to it. The SDK's `PermissionDenied`
|
|
215
|
+
* hook is a different thing that belongs to the SDK's own prompt path and does not fire for a
|
|
216
|
+
* hook-authored deny — measured — so naming it here would put an event in the trace that never
|
|
217
|
+
* happened, and would make a reader grepping traces conclude the hook fires.
|
|
218
|
+
*/
|
|
219
|
+
export const CONTROL_EVENTS = [
|
|
220
|
+
'create_requested',
|
|
221
|
+
'prompt_submitted',
|
|
222
|
+
'interrupt_requested',
|
|
223
|
+
'stop_requested',
|
|
224
|
+
'permission_denied',
|
|
225
|
+
] as const;
|
|
226
|
+
|
|
227
|
+
export type ControlEventName = (typeof CONTROL_EVENTS)[number];
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* The agent process's own lifecycle, as causes.
|
|
231
|
+
*
|
|
232
|
+
* These subsume `SESSION_END_CAUSES` deliberately. `sessions/session.ts` states four end causes
|
|
233
|
+
* local to the handle, and its own comment says the layer owning the declared state model maps them
|
|
234
|
+
* onto its causes. This is that layer, and this is that mapping — one cause vocabulary, not two.
|
|
235
|
+
* `state/reconciliation.test.ts` asserts the containment so it cannot drift.
|
|
236
|
+
*/
|
|
237
|
+
export const PROCESS_EVENTS = [
|
|
238
|
+
'process_started',
|
|
239
|
+
'stop_requested',
|
|
240
|
+
'process_ended',
|
|
241
|
+
'process_failed',
|
|
242
|
+
'start_timed_out',
|
|
243
|
+
] as const;
|
|
244
|
+
|
|
245
|
+
export type ProcessEventName = (typeof PROCESS_EVENTS)[number];
|
|
246
|
+
|
|
247
|
+
export const TIMEOUT_EVENTS = ['start_timed_out', 'hook_timed_out'] as const;
|
|
248
|
+
|
|
249
|
+
export type TimeoutEventName = (typeof TIMEOUT_EVENTS)[number];
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
* The gate's expiry, named once.
|
|
253
|
+
*
|
|
254
|
+
* One concept with two spellings — `hook_timed_out` on the transition, and `hook-timed-out` as
|
|
255
|
+
* the prefix of the deny reason handed to the model — looks exactly like a declared refusal reason
|
|
256
|
+
* (its sibling branch really is one) while resolving to nothing, so a reader who looks it up finds
|
|
257
|
+
* no such name. `satisfies` ties it to the closed vocabulary above, so the two cannot drift apart
|
|
258
|
+
* without breaking the build.
|
|
259
|
+
*/
|
|
260
|
+
export const HOOK_TIMEOUT_EVENT = 'hook_timed_out' satisfies TimeoutEventName;
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* Every name a cause may carry. Closed, so a transition that cannot name its cause does not
|
|
264
|
+
* compile — the strongest available form of "cause is not nullable".
|
|
265
|
+
*/
|
|
266
|
+
export type CauseEvent =
|
|
267
|
+
HookEventName | MessageEventName | ControlEventName | ProcessEventName | TimeoutEventName | RefusalReason;
|
|
268
|
+
|
|
269
|
+
const CAUSE_EVENT_SET: ReadonlySet<string> = new Set<string>([
|
|
270
|
+
...HOOK_EVENTS,
|
|
271
|
+
...MESSAGE_EVENTS,
|
|
272
|
+
...CONTROL_EVENTS,
|
|
273
|
+
...PROCESS_EVENTS,
|
|
274
|
+
...TIMEOUT_EVENTS,
|
|
275
|
+
]);
|
|
276
|
+
|
|
277
|
+
/** The runtime half. A frame decoded from the wire was never seen by the compiler. */
|
|
278
|
+
export function isCauseEvent(value: string): value is CauseEvent {
|
|
279
|
+
return CAUSE_EVENT_SET.has(value) || isRefusalReason(value);
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
export function isCauseKind(value: string): value is CauseKind {
|
|
283
|
+
return (CAUSE_KINDS as readonly string[]).includes(value);
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
export interface TransitionCause {
|
|
287
|
+
readonly kind: CauseKind;
|
|
288
|
+
/** The literal hook, message or control event that fired. Never free text. */
|
|
289
|
+
readonly event: CauseEvent;
|
|
290
|
+
/** For a human reading a log. Never branched on — that is what `kind` and `event` are for. */
|
|
291
|
+
readonly detail: string;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
// ---------------------------------------------------------------------------
|
|
295
|
+
// where
|
|
296
|
+
// ---------------------------------------------------------------------------
|
|
297
|
+
|
|
298
|
+
/**
|
|
299
|
+
* Which session, which worktree, which branch — so the trace answers "where" without a transcript.
|
|
300
|
+
*
|
|
301
|
+
* A null `worktree` or `branch` is a named outcome: `unknownReason` says which of the ordinary
|
|
302
|
+
* situations produced it (not a repository, detached HEAD, unreadable HEAD). An empty string would
|
|
303
|
+
* make "not in a repository" and "in a repository whose HEAD could not be read" the same value.
|
|
304
|
+
*/
|
|
305
|
+
export interface TransitionWhere {
|
|
306
|
+
readonly cwd: string;
|
|
307
|
+
readonly worktree: string | null;
|
|
308
|
+
readonly branch: string | null;
|
|
309
|
+
readonly unknownReason: string | null;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
// ---------------------------------------------------------------------------
|
|
313
|
+
// the transition record
|
|
314
|
+
// ---------------------------------------------------------------------------
|
|
315
|
+
|
|
316
|
+
/**
|
|
317
|
+
* One recorded transition. The shape follows `control/link-state.ts`'s `LinkTransition` on purpose
|
|
318
|
+
* — `{from, to, cause, at, detail}` with a required cause is this package's established shape for a
|
|
319
|
+
* state machine, and two shapes for one idea is how vocabularies drift apart.
|
|
320
|
+
*/
|
|
321
|
+
export interface SessionTransition {
|
|
322
|
+
/**
|
|
323
|
+
* The agent's own session id — null until it has reported itself, and never null again after.
|
|
324
|
+
*
|
|
325
|
+
* The host does not mint one. An agent emits nothing until a turn is queued, so there is a
|
|
326
|
+
* real window with a live process and no id, and inventing a name for it would be exactly the
|
|
327
|
+
* "coin a word by observation" failure this package is written against. A controller that needs
|
|
328
|
+
* to correlate its request with the session it became supplies `correlationId`.
|
|
329
|
+
*/
|
|
330
|
+
readonly sessionId: string | null;
|
|
331
|
+
/** Dense from 1, per session-machine. A gap is detectable by arithmetic alone. */
|
|
332
|
+
readonly seq: number;
|
|
333
|
+
readonly at: string;
|
|
334
|
+
readonly from: SessionState;
|
|
335
|
+
readonly to: SessionState;
|
|
336
|
+
/** What the session is blocked on after this transition. Null when nothing holds it. */
|
|
337
|
+
readonly activity: SessionActivity | null;
|
|
338
|
+
/** The open entry this transition opened, closed or backgrounded. Null when it touched none. */
|
|
339
|
+
readonly entryId: string | null;
|
|
340
|
+
readonly cause: TransitionCause;
|
|
341
|
+
readonly where: TransitionWhere;
|
|
342
|
+
/**
|
|
343
|
+
* Opaque, controller-supplied, and never interpreted here.
|
|
344
|
+
*
|
|
345
|
+
* It exists so a controller can tie a session back to whatever it means on its side. The host
|
|
346
|
+
* does not parse it, branch on it, or derive anything from it — the moment it did, this package
|
|
347
|
+
* would know something about one product's conventions and stop being general.
|
|
348
|
+
*/
|
|
349
|
+
readonly correlationId: string | null;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
// ---------------------------------------------------------------------------
|
|
353
|
+
// open entries
|
|
354
|
+
// ---------------------------------------------------------------------------
|
|
355
|
+
|
|
356
|
+
/**
|
|
357
|
+
* Foreground work holds the session; background work does not.
|
|
358
|
+
*
|
|
359
|
+
* A backgrounded task produces an entry that never exits while the turn moves on — without this
|
|
360
|
+
* split, a session that starts a long build reads as blocked on it forever. The move happens at the
|
|
361
|
+
* moment the SDK reports the task backgrounded, with that as the cause; never on a timer and never
|
|
362
|
+
* by inference. A backgrounded entry is not a finished one: it stays open, it keeps ageing, and it
|
|
363
|
+
* still gets an exit.
|
|
364
|
+
*/
|
|
365
|
+
export type EntryLane = 'foreground' | 'background';
|
|
366
|
+
|
|
367
|
+
export interface OpenEntry {
|
|
368
|
+
/** The SDK's own id for the thing — `tool_use_id`, `agent_id`, `task_id`. Never minted here. */
|
|
369
|
+
readonly entryId: string;
|
|
370
|
+
readonly activity: SessionActivity;
|
|
371
|
+
readonly lane: EntryLane;
|
|
372
|
+
readonly openedAt: string;
|
|
373
|
+
/** When it stopped holding the session. Null while it still does. */
|
|
374
|
+
readonly backgroundedAt: string | null;
|
|
375
|
+
/**
|
|
376
|
+
* When cleanup marked it abandoned. Cleanup may mark; it may never erase.
|
|
377
|
+
*
|
|
378
|
+
* A session sitting in one tool call for forty minutes is the most useful thing this model can
|
|
379
|
+
* report, and a reconciler that quietly closes the entry destroys exactly that signal. So an
|
|
380
|
+
* unpaired entry is surfaced with its age and a reason, and the fact that it happened survives.
|
|
381
|
+
*/
|
|
382
|
+
readonly abandonedAt: string | null;
|
|
383
|
+
readonly abandonReason: string | null;
|
|
384
|
+
/** Why it opened. Carried so an entry read weeks later still says what started it. */
|
|
385
|
+
readonly cause: TransitionCause;
|
|
386
|
+
/** Set when the entry belongs to a subagent rather than the main thread. */
|
|
387
|
+
readonly agentId: string | null;
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
/** An open entry with the number a human actually wants. */
|
|
391
|
+
export interface AgedEntry extends OpenEntry {
|
|
392
|
+
readonly ageMs: number;
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
// ---------------------------------------------------------------------------
|
|
396
|
+
// what this host can say about its own sessions
|
|
397
|
+
// ---------------------------------------------------------------------------
|
|
398
|
+
|
|
399
|
+
/**
|
|
400
|
+
* One session, as this host sees it.
|
|
401
|
+
*
|
|
402
|
+
* This is not a roster and must not grow into one. A roster spans every session everywhere, and
|
|
403
|
+
* sessions live on different hosts — an agent runs where its host runs, so no single host can
|
|
404
|
+
* produce one. This enumerates the sessions this host holds, which is the raw material a controller
|
|
405
|
+
* aggregates into a roster on its side. If this grows a filter, a search or a notion of what a
|
|
406
|
+
* session means, the boundary has been crossed.
|
|
407
|
+
*/
|
|
408
|
+
export interface SessionSnapshot {
|
|
409
|
+
readonly sessionId: string | null;
|
|
410
|
+
readonly correlationId: string | null;
|
|
411
|
+
readonly state: SessionState;
|
|
412
|
+
readonly activity: SessionActivity | null;
|
|
413
|
+
readonly where: TransitionWhere;
|
|
414
|
+
readonly openEntries: readonly AgedEntry[];
|
|
415
|
+
readonly lastTransitionAt: string | null;
|
|
416
|
+
readonly transitionCount: number;
|
|
417
|
+
}
|