@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.
- package/LICENSE +21 -0
- package/README.md +22 -1
- package/package.json +32 -7
- package/src/claude-code-session.ts +252 -109
- package/src/claude-usage.ts +30 -0
- package/src/codex-session.ts +521 -130
- package/src/harness-session.ts +121 -10
- package/src/index.ts +2 -0
- package/src/retained-evidence.ts +20 -0
- package/src/turn-state.ts +42 -0
package/src/harness-session.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
-
/**
|
|
53
|
-
readonly tokens?:
|
|
54
|
-
/**
|
|
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?:
|
|
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:
|
|
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<
|
|
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?:
|
|
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
|
-
/**
|
|
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
|
@@ -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
|
+
}
|