@inixiative/agent-session 0.1.0 → 0.2.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.
@@ -17,7 +17,50 @@
17
17
  // Event taxonomy — classified events from the agent stream
18
18
  // ---------------------------------------------------------------------------
19
19
 
20
+ /** IDs are native only when observed. admissionId is generated locally, never a native turn ID. */
21
+ export interface SessionIdentity {
22
+ readonly admissionId?: string;
23
+ readonly nativeSessionId?: string;
24
+ readonly threadId?: string;
25
+ readonly turnId?: string;
26
+ readonly itemId?: string;
27
+ readonly callId?: string;
28
+ readonly messageId?: string;
29
+ readonly rpcRequestId?: number;
30
+ readonly correlation?: "native-turn" | "ordered-stream" | "unknown";
31
+ }
32
+ export type NativeOutcome = "unknown" | "completed" | "failed";
33
+ export interface NativeTerminal {
34
+ readonly type: string;
35
+ readonly eventId?: string;
36
+ readonly turnId?: string;
37
+ readonly subtype?: string;
38
+ readonly reason?: string;
39
+ readonly apiErrorStatus?: number;
40
+ }
41
+ export interface SessionAttempt extends SessionIdentity {
42
+ readonly nativeOutcome: NativeOutcome;
43
+ readonly localOutcome: "pending" | "resolved" | "rejected";
44
+ readonly dispatch: "not-dispatched" | "attempted";
45
+ readonly transportOutcome: "open" | "failed" | "closed";
46
+ readonly terminal?: NativeTerminal;
47
+ readonly rpcOutcome?: "pending" | "resolved" | "failed" | "unknown";
48
+ readonly localFailure?: "timeout" | "interrupt-request" | "transport" | "rpc" | "validation" | "blocked" | "killed" | "unrecognized-terminal" | "registration";
49
+ readonly content: string;
50
+ readonly events: readonly SessionEvent[];
51
+ readonly tokens?: SessionTokens;
52
+ }
53
+ /** A rejected local send can still have a later native terminal; read attempt again to reconcile. */
54
+ export class SessionTurnError extends Error {
55
+ constructor(message: string, private readonly evidence: () => SessionAttempt, options?: ErrorOptions) {
56
+ super(message, options); this.name = "SessionTurnError";
57
+ }
58
+ get attempt(): SessionAttempt { return this.evidence(); }
59
+ }
60
+
20
61
  export type SessionEventKind =
62
+ | "text_delta"
63
+ | "native_status"
21
64
  | "session_start"
22
65
  | "session_end"
23
66
  | "session_compact"
@@ -25,20 +68,63 @@ export type SessionEventKind =
25
68
  | "tool_use"
26
69
  | "tool_result"
27
70
  | "thinking"
71
+ | "usage"
28
72
  | "result"
29
73
  | "error";
30
74
 
31
- export interface SessionEvent {
75
+ /** Input excludes cache reads/writes when those disjoint counters are supplied.
76
+ * Thinking is a subset of output; TTL counters are subsets of cacheWrite.
77
+ * Missing optional counters mean the runtime did not report them.
78
+ */
79
+ export interface SessionTokens {
80
+ input: number;
81
+ output: number;
82
+ cacheRead?: number;
83
+ cacheWrite?: number;
84
+ cacheWrite5m?: number;
85
+ cacheWrite1h?: number;
86
+ thinking?: number;
87
+ /** Original provider usage, including tags and future fields. Never summed. */
88
+ providerUsage?: Readonly<Record<string, unknown>>;
89
+ }
90
+
91
+ export interface SessionEvent extends SessionIdentity {
92
+ /**
93
+ * Session-history evidence excluded from admission results and state transitions.
94
+ * "session-configuration": a native lifecycle/configuration envelope (Claude system/init)
95
+ * retained with its binding; it belongs to the session, never to an admission, and a
96
+ * repeated one is not evidence of compaction or restart. "duplicate-boundary": an explicit
97
+ * compaction boundary whose native uuid was already recorded.
98
+ */
99
+ readonly unattributedReason?: "no-admission" | "foreign-session" | "foreign-turn" | "duplicate-terminal" | "after-terminal"
100
+ | "unrecognized-event" | "session-configuration" | "duplicate-boundary" | "duplicate-tool" | "unmatched-tool" | "malformed-tool";
101
+ /** Local transport observation, not a native terminal acknowledgment. */
102
+ readonly transportOutcome?: SessionAttempt["transportOutcome"];
103
+ readonly nativeOutcome?: NativeOutcome;
104
+ readonly terminal?: NativeTerminal;
32
105
  readonly kind: SessionEventKind;
33
106
  readonly timestamp: number;
34
107
  /** Text content (for text, thinking, result, error). */
35
108
  readonly text?: string;
36
109
  /** Tool name (for tool_use). */
37
110
  readonly toolName?: string;
111
+ /** Native MCP invocation identity, separate from call ID and SDK request IDs.
112
+ * Display name is mcp__<server>__<tool>; use these fields for exact comparisons. */
113
+ readonly toolServer?: string;
114
+ readonly toolMethod?: string;
38
115
  /** Tool input arguments (for tool_use). */
39
116
  readonly toolInput?: Record<string, unknown>;
40
117
  /** Tool output (for tool_result). */
41
118
  readonly toolOutput?: string;
119
+ /** Public text content is retained; unsupported/non-text result payload is omitted. */
120
+ readonly toolOutputOmitted?: boolean;
121
+ /** Exact public tool names from `tool_reference` result blocks (tool search), in order,
122
+ * duplicates preserved; a separately owned guard judges ownership. Absent when none. */
123
+ readonly toolReferences?: readonly string[];
124
+ /** Public type labels of omitted non-text result blocks, in first-seen order; unknown or
125
+ * malformed shapes are "unsupported". Present only when toolOutputOmitted is true. */
126
+ readonly toolOutputOmittedTypes?: readonly string[];
127
+ readonly toolInputOmitted?: string;
42
128
  /** Whether the tool call failed (for tool_result). */
43
129
  readonly toolError?: boolean;
44
130
  /** Compaction source runtime (for session_compact): "claude-code" | "codex" | etc. */
@@ -49,30 +135,43 @@ export interface SessionEvent {
49
135
  * this is NOT the Foundry thread ID. A SessionAdapter maps between the two.
50
136
  */
51
137
  readonly externalSessionId?: string;
52
- /** Token usage (for result). */
53
- readonly tokens?: { input: number; output: number };
54
- /** Raw message from the stream (for Oracle introspection). */
138
+ /** Turn totals on result; request snapshots on usage. Do not sum both. */
139
+ readonly tokens?: SessionTokens;
140
+ /** Owned, deeply frozen native JSON data (for Oracle introspection). */
55
141
  readonly raw?: unknown;
56
142
  }
57
143
 
58
144
  /** Result of a single send() — the turn's content plus all classified events. */
59
- export interface SessionResult {
145
+ export interface SessionResult extends SessionIdentity {
146
+ readonly localOutcome?: SessionAttempt["localOutcome"];
147
+ readonly transportOutcome?: SessionAttempt["transportOutcome"];
148
+ readonly rpcOutcome?: SessionAttempt["rpcOutcome"];
149
+ readonly nativeOutcome?: NativeOutcome;
150
+ readonly terminal?: NativeTerminal;
60
151
  readonly content: string;
61
152
  readonly events: readonly SessionEvent[];
62
- readonly tokens?: { input: number; output: number };
153
+ readonly tokens?: SessionTokens;
63
154
  /** The runtime's native session ID. See SessionEvent.externalSessionId. */
64
155
  readonly externalSessionId?: string;
65
156
  }
66
157
 
67
158
  /** Full session record for Oracle evaluation. */
159
+ export interface SessionDiagnostics {
160
+ /** Counts only: observer exception messages/objects are never retained. */
161
+ readonly observerFailures: { readonly synchronous: number; readonly asynchronous: number };
162
+ }
68
163
  export interface SessionArtifact {
164
+ readonly diagnostics?: SessionDiagnostics;
165
+ readonly attempts?: readonly SessionAttempt[];
166
+ /** Legacy totalTokens contains observed counts only, not proof of complete accounting. */
167
+ readonly accounting?: "observed-only";
69
168
  /** The runtime's native session ID. See SessionEvent.externalSessionId. */
70
169
  readonly externalSessionId?: string;
71
170
  readonly events: readonly SessionEvent[];
72
171
  readonly startedAt: number;
73
172
  readonly endedAt?: number;
74
173
  readonly turns: number;
75
- readonly totalTokens: { input: number; output: number };
174
+ readonly totalTokens: SessionTokens;
76
175
  readonly toolCalls: number;
77
176
  readonly toolResults: number;
78
177
  readonly errors: number;
@@ -94,11 +193,23 @@ export type BeforeSendHook = (
94
193
  message: string,
95
194
  ) => string | Promise<string>;
96
195
 
196
+ export interface SessionSendOptions {
197
+ timeout?: number;
198
+ /** Required ownership registration, not an observer. Rejection prevents the native write. */
199
+ onAdmission?: (attempt: SessionAttempt) => void | Promise<void>;
200
+ }
201
+
97
202
  // ---------------------------------------------------------------------------
98
203
  // HarnessSession interface
99
204
  // ---------------------------------------------------------------------------
100
205
 
101
206
  export interface HarnessSession {
207
+ /** One immutable admission snapshot, without copying unrelated session history. */
208
+ inspectAttempt?(admissionId: string): SessionAttempt | undefined;
209
+ readonly admissionProtocol?: "prewrite-v1";
210
+ readonly diagnostics?: SessionDiagnostics;
211
+ readonly attempts?: readonly SessionAttempt[];
212
+ readonly accounting?: "observed-only";
102
213
  readonly alive: boolean;
103
214
  /**
104
215
  * The agent runtime's native session ID — external to Foundry.
@@ -109,13 +220,13 @@ export interface HarnessSession {
109
220
  readonly externalSessionId: string | undefined;
110
221
  readonly events: readonly SessionEvent[];
111
222
  readonly turns: number;
112
- readonly totalTokens: Readonly<{ input: number; output: number }>;
223
+ readonly totalTokens: Readonly<SessionTokens>;
113
224
 
114
225
  /** Spawn the underlying process. Must be called before send(). */
115
226
  start(): Promise<void>;
116
227
 
117
228
  /** Send a message. Queued if another turn is in-flight. */
118
- send(message: string, opts?: { timeout?: number }): Promise<SessionResult>;
229
+ send(message: string, opts?: SessionSendOptions): Promise<SessionResult>;
119
230
 
120
231
  /**
121
232
  * Fork: create a new (unstarted) session branching from current state.
@@ -123,7 +234,7 @@ export interface HarnessSession {
123
234
  */
124
235
  fork(opts?: { cwd?: string; baseContext?: string }): HarnessSession;
125
236
 
126
- /** Interrupt the current in-flight turn (best-effort). */
237
+ /** Reject the local waiter. No native cancellation acknowledgment is currently implemented. */
127
238
  interrupt(): void;
128
239
 
129
240
  /** Kill the session process and reject any pending turns. */
package/src/index.ts CHANGED
@@ -19,3 +19,5 @@ export {
19
19
  type CodexSessionConfig,
20
20
  type CodexSpawn,
21
21
  } from "./codex-session";
22
+
23
+ export { parseClaudeUsage } from "./claude-usage";
@@ -0,0 +1,20 @@
1
+ // Native envelopes are JSON data. Take ownership once, before exposing them to
2
+ // observers, then share deeply frozen evidence across histories and snapshots.
3
+ // Do not traverse/copy the accumulated transcript when a new event arrives.
4
+ const owned = new WeakSet<object>();
5
+
6
+ export function retainEvidence<T>(value: T): T {
7
+ if (value === null || typeof value !== "object" || owned.has(value)) return value;
8
+ const copy = structuredClone(value);
9
+ const pending: object[] = [copy];
10
+ while (pending.length) {
11
+ const item = pending.pop()!;
12
+ if (owned.has(item)) continue;
13
+ owned.add(item);
14
+ for (const nested of Object.values(item)) {
15
+ if (nested !== null && typeof nested === "object") pending.push(nested);
16
+ }
17
+ Object.freeze(item);
18
+ }
19
+ return copy;
20
+ }
@@ -0,0 +1,42 @@
1
+ import { SessionTurnError, type SessionAttempt, type SessionEvent, type SessionIdentity, type SessionResult, type SessionTokens } from "./harness-session";
2
+ import { retainEvidence } from "./retained-evidence";
3
+ /** Per-admission evidence. Caller settlement never transfers native ownership. */
4
+ export class TurnState {
5
+ readonly admissionId = crypto.randomUUID();
6
+ identity: SessionIdentity = {};
7
+ nativeOutcome: SessionAttempt["nativeOutcome"] = "unknown";
8
+ localOutcome: SessionAttempt["localOutcome"] = "pending";
9
+ dispatch: SessionAttempt["dispatch"] = "not-dispatched";
10
+ transportOutcome: SessionAttempt["transportOutcome"] = "open";
11
+ private _terminal?: SessionAttempt["terminal"];
12
+ get terminal() { return this._terminal; }
13
+ set terminal(value: SessionAttempt["terminal"]) { this._terminal = retainEvidence(value); }
14
+ events: SessionEvent[] = [];
15
+ content = "";
16
+ private _tokens?: SessionTokens;
17
+ get tokens() { return this._tokens; }
18
+ set tokens(value: SessionTokens | undefined) { this._tokens = retainEvidence(value); }
19
+ localFailure?: SessionAttempt["localFailure"];
20
+ rpcSettled = false;
21
+ rpcOutcome?: SessionAttempt["rpcOutcome"];
22
+ snapshot(): SessionAttempt {
23
+ return Object.freeze({ ...this.identity, admissionId: this.admissionId, nativeOutcome: this.nativeOutcome,
24
+ localOutcome: this.localOutcome, dispatch: this.dispatch, transportOutcome: this.transportOutcome,
25
+ rpcOutcome: this.rpcOutcome, terminal: this.terminal, localFailure: this.localFailure,
26
+ content: this.content, events: Object.freeze([...this.events]), tokens: this.tokens });
27
+ }
28
+ result(externalSessionId?: string): SessionResult { return Object.freeze({ ...this.snapshot(), externalSessionId }); }
29
+ fail(error: Error, reason: SessionAttempt["localFailure"]): SessionTurnError {
30
+ if (this.localOutcome === "pending") { this.localOutcome = "rejected"; this.localFailure = reason; }
31
+ return new SessionTurnError(error.message, () => this.snapshot(), { cause: error });
32
+ }
33
+ record(event: SessionEvent): SessionEvent {
34
+ const e = retainEvidence({ ...this.identity, ...event, admissionId: this.admissionId });
35
+ this.events.push(e);
36
+ if ((e.kind === "text" || e.kind === "result") && e.text) this.content = e.text;
37
+ if (e.kind === "text_delta" && e.text) this.content += e.text;
38
+ // Usage events are per-request snapshots; the terminal result carries the turn total.
39
+ if (e.tokens && e.kind !== "usage") this.tokens = e.tokens;
40
+ return e;
41
+ }
42
+ }