@inixiative/foundry-core 0.1.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/src/trace.ts ADDED
@@ -0,0 +1,261 @@
1
+ import type { TokenCounts } from "./token-counts";
2
+ /**
3
+ * A single span in a message's journey through the system.
4
+ * Each stage (classify, route, dispatch, writeback, middleware) creates a span.
5
+ */
6
+ export interface Span {
7
+ readonly id: string;
8
+ readonly parentId?: string;
9
+ readonly name: string;
10
+ readonly kind: SpanKind;
11
+ readonly agentId?: string;
12
+ readonly threadId?: string;
13
+
14
+ /** What layers were visible to this stage. */
15
+ readonly layerIds?: string[];
16
+
17
+ /** Context hash at the time of this span. */
18
+ readonly contextHash?: string;
19
+
20
+ /** Input to this stage. */
21
+ readonly input?: unknown;
22
+
23
+ /** Output from this stage. */
24
+ readonly output?: unknown;
25
+
26
+ readonly startedAt: number;
27
+ endedAt?: number;
28
+ durationMs?: number;
29
+
30
+ /** Status of this span. */
31
+ status: SpanStatus;
32
+
33
+ /** If errored, the error detail. */
34
+ error?: unknown;
35
+
36
+ /** Token usage for this span's LLM call (if any). */
37
+ tokens?: TokenCounts;
38
+
39
+ /** Estimated cost in dollars for this span's LLM call. */
40
+ cost?: number;
41
+
42
+ /** Arbitrary annotations from middleware or handlers. */
43
+ annotations: Record<string, unknown>;
44
+
45
+ /** Child spans (for nested dispatch, fan-out, etc.) */
46
+ children: Span[];
47
+ }
48
+
49
+ export type SpanKind =
50
+ | "ingress"
51
+ | "classify"
52
+ | "route"
53
+ | "dispatch"
54
+ | "execute"
55
+ | "decide"
56
+ | "middleware"
57
+ | "writeback"
58
+ | "egress"
59
+ | "fan"
60
+ | (string & {});
61
+
62
+ export type SpanStatus = "running" | "ok" | "error";
63
+
64
+ import { newId } from "./id";
65
+
66
+ function nextSpanId(): string {
67
+ return newId("span");
68
+ }
69
+
70
+ /**
71
+ * A Trace is the full journey of a single message through the system.
72
+ *
73
+ * Created at ingress, it accumulates spans as the message flows through
74
+ * classify → route → dispatch → middleware → execute → writeback → egress.
75
+ * Each span can have child spans (e.g. a dispatch span contains an execute span,
76
+ * a fan span contains multiple dispatch children).
77
+ *
78
+ * The UI renders a Trace as a drillable tree — click any span to see
79
+ * what layers it saw, what it received, what it produced, how long it took.
80
+ */
81
+ export class Trace {
82
+ readonly id: string;
83
+ readonly messageId: string;
84
+ readonly startedAt: number;
85
+ endedAt?: number;
86
+
87
+ /** The root span — usually "ingress". Children branch from here. */
88
+ readonly root: Span;
89
+
90
+ /** Flat index of all spans for quick lookup. */
91
+ private _spans: Map<string, Span> = new Map();
92
+
93
+ /** Current active span stack — the innermost is where new children attach. */
94
+ private _stack: Span[] = [];
95
+
96
+ constructor(messageId: string, detail?: Pick<Span, "input" | "threadId">) {
97
+ this.id = newId("trace");
98
+ this.messageId = messageId;
99
+ this.startedAt = performance.now();
100
+
101
+ this.root = this._createSpan("ingress", "ingress", undefined, detail);
102
+ this._stack.push(this.root);
103
+ }
104
+
105
+ /** Start a new span as a child of the current active span. */
106
+ start(name: string, kind: SpanKind, detail?: Partial<Span>): Span {
107
+ const parent = this._stack[this._stack.length - 1];
108
+ const span = this._createSpan(name, kind, parent?.id, detail);
109
+ if (parent) {
110
+ parent.children.push(span);
111
+ }
112
+ this._stack.push(span);
113
+ return span;
114
+ }
115
+
116
+ /** End the current active span. */
117
+ end(output?: unknown, error?: unknown): Span | undefined {
118
+ const span = this._stack.pop();
119
+ if (!span) return undefined;
120
+
121
+ span.endedAt = performance.now();
122
+ span.durationMs = span.endedAt - span.startedAt;
123
+
124
+ if (error) {
125
+ span.status = "error";
126
+ span.error = error;
127
+ } else {
128
+ span.status = "ok";
129
+ }
130
+
131
+ if (output !== undefined) {
132
+ (span as { output: unknown }).output = output;
133
+ }
134
+
135
+ return span;
136
+ }
137
+
138
+ /** End the entire trace. Closes any remaining open spans. */
139
+ finish(): void {
140
+ const now = performance.now();
141
+ // Close any unclosed spans — preserve their current status if already set
142
+ while (this._stack.length > 0) {
143
+ const span = this._stack.pop()!;
144
+ if (!span.endedAt) {
145
+ span.endedAt = now;
146
+ span.durationMs = span.endedAt - span.startedAt;
147
+ }
148
+ if (span.status === "running") {
149
+ span.status = "ok";
150
+ }
151
+ }
152
+ this.endedAt = now;
153
+ }
154
+
155
+ /** Get a span by id. */
156
+ getSpan(id: string): Span | undefined {
157
+ return this._spans.get(id);
158
+ }
159
+
160
+ /** All spans as a flat list, ordered by start time. */
161
+ get spans(): ReadonlyArray<Span> {
162
+ return [...this._spans.values()].sort((a, b) => a.startedAt - b.startedAt);
163
+ }
164
+
165
+ /** Total duration of the trace. */
166
+ get durationMs(): number | undefined {
167
+ return this.endedAt ? this.endedAt - this.startedAt : undefined;
168
+ }
169
+
170
+ /** The current active span (innermost). */
171
+ get current(): Span | undefined {
172
+ return this._stack[this._stack.length - 1];
173
+ }
174
+
175
+ /** How deep we are in the span stack. */
176
+ get depth(): number {
177
+ return this._stack.length;
178
+ }
179
+
180
+ /**
181
+ * Produce a summary for display — the key stages and their outcomes.
182
+ * This is what shows up in a message list before you drill in.
183
+ */
184
+ summary(): TraceSummary {
185
+ const stages: StageSummary[] = [];
186
+
187
+ const walk = (span: Span, depth: number) => {
188
+ stages.push({
189
+ name: span.name,
190
+ kind: span.kind,
191
+ status: span.status,
192
+ durationMs: span.durationMs,
193
+ agentId: span.agentId,
194
+ tokens: span.tokens,
195
+ cost: span.cost,
196
+ depth,
197
+ });
198
+ for (const child of span.children) {
199
+ walk(child, depth + 1);
200
+ }
201
+ };
202
+
203
+ walk(this.root, 0);
204
+
205
+ return {
206
+ traceId: this.id,
207
+ messageId: this.messageId,
208
+ totalDurationMs: this.durationMs,
209
+ spanCount: this._spans.size,
210
+ stages,
211
+ };
212
+ }
213
+
214
+ // -- Internal --
215
+
216
+ private _createSpan(
217
+ name: string,
218
+ kind: SpanKind,
219
+ parentId?: string,
220
+ detail?: Partial<Span>
221
+ ): Span {
222
+ const span: Span = {
223
+ id: nextSpanId(),
224
+ parentId,
225
+ name,
226
+ kind,
227
+ agentId: detail?.agentId,
228
+ threadId: detail?.threadId,
229
+ layerIds: detail?.layerIds,
230
+ contextHash: detail?.contextHash,
231
+ input: detail?.input,
232
+ output: undefined,
233
+ startedAt: performance.now(),
234
+ status: "running",
235
+ annotations: detail?.annotations ?? {},
236
+ children: [],
237
+ };
238
+
239
+ this._spans.set(span.id, span);
240
+ return span;
241
+ }
242
+ }
243
+
244
+ export interface TraceSummary {
245
+ readonly traceId: string;
246
+ readonly messageId: string;
247
+ readonly totalDurationMs?: number;
248
+ readonly spanCount: number;
249
+ readonly stages: StageSummary[];
250
+ }
251
+
252
+ export interface StageSummary {
253
+ readonly name: string;
254
+ readonly kind: SpanKind;
255
+ readonly status: SpanStatus;
256
+ readonly durationMs?: number;
257
+ readonly agentId?: string;
258
+ readonly tokens?: TokenCounts;
259
+ readonly cost?: number;
260
+ readonly depth: number;
261
+ }
package/src/types.ts ADDED
@@ -0,0 +1,167 @@
1
+ import type { TokenCounts } from "./token-counts";
2
+ // ---------------------------------------------------------------------------
3
+ // Shared types — used by harness for conditional invocation/activation
4
+ // ---------------------------------------------------------------------------
5
+
6
+ /**
7
+ * Condition for conditional invocation/activation.
8
+ * Matches when ANY specified field matches (OR across fields, OR within arrays).
9
+ */
10
+ export interface InvocationCondition {
11
+ /** Match if classification.category is one of these. */
12
+ categories?: string[];
13
+ /** Match if any classification tag overlaps with these. */
14
+ tags?: string[];
15
+ /** Match if route.destination is one of these. */
16
+ routes?: string[];
17
+ }
18
+
19
+ /**
20
+ * Minimal agent mode config — what the harness needs from config.
21
+ * Full AgentSettingsConfig lives in @inixiative/foundry.
22
+ */
23
+ export interface AgentModeConfig {
24
+ invocation?: "always" | "on-demand" | "conditional";
25
+ condition?: InvocationCondition;
26
+ }
27
+
28
+ /**
29
+ * Minimal layer mode config — what the harness needs from config.
30
+ * Full LayerSettingsConfig lives in @inixiative/foundry.
31
+ */
32
+ export interface LayerModeConfig {
33
+ activation?: "always" | "on-demand" | "conditional";
34
+ condition?: InvocationCondition;
35
+ }
36
+
37
+ // ---------------------------------------------------------------------------
38
+ // LLM provider interface — the abstraction, not implementations
39
+ // ---------------------------------------------------------------------------
40
+
41
+ export interface LLMMessage {
42
+ readonly role: "system" | "user" | "assistant";
43
+ readonly content: string;
44
+ }
45
+
46
+ /** Tool definition sent to the LLM so it can call tools. */
47
+ export interface ToolDefinition {
48
+ /** Tool name (e.g., "shell_exec", "memory_search"). */
49
+ name: string;
50
+ /** Short description for the LLM. */
51
+ description: string;
52
+ /** JSON Schema for the tool's input parameters. */
53
+ inputSchema: Record<string, unknown>;
54
+ }
55
+
56
+ /** A tool call returned by the LLM. */
57
+ export interface ToolCall {
58
+ /** Provider-assigned call ID (for matching results). */
59
+ id: string;
60
+ /** Tool name to invoke. */
61
+ name: string;
62
+ /** Parsed input arguments. */
63
+ input: Record<string, unknown>;
64
+ }
65
+
66
+ /** Result of executing a tool call, fed back to the LLM. */
67
+ export interface ToolCallResult {
68
+ /** Matches the ToolCall.id. */
69
+ toolCallId: string;
70
+ /** Serialized result content. */
71
+ content: string;
72
+ /** Whether the tool call succeeded. */
73
+ isError?: boolean;
74
+ }
75
+
76
+ export interface CompletionOpts {
77
+ model?: string;
78
+ maxTokens?: number;
79
+ temperature?: number;
80
+ topP?: number;
81
+ stop?: string[];
82
+ /** Max agentic turns (for runtimes that support tool use). */
83
+ maxTurns?: number | null;
84
+ /** Additive native ownership contract; optional for standalone/legacy providers. */
85
+ nativeObservation?: import("./native-evidence").NativeObservation;
86
+ /** Enable/disable tool use. False = pure text completion. */
87
+ tools?: boolean;
88
+ /** Tool definitions for the LLM to call. Providers map to native format. */
89
+ toolDefinitions?: ToolDefinition[];
90
+
91
+ // -- Extended knobs (providers map these to their native APIs) --
92
+
93
+ /**
94
+ * Extended thinking / reasoning effort level.
95
+ * Providers translate: Anthropic → thinking.budget_tokens, OpenAI → reasoning_effort.
96
+ * "none" = no thinking, "low"/"medium"/"high" = increasing effort, number = explicit budget tokens.
97
+ */
98
+ thinking?: "none" | "low" | "medium" | "high" | number;
99
+ /** Permission level for code execution runtimes (claude-code, codex). */
100
+ permissions?: "bypass" | "supervised" | "restricted";
101
+ /** Per-call timeout in ms. Provider uses its own default if omitted. */
102
+ timeout?: number;
103
+ /** Enable prompt caching where supported. */
104
+ cacheControl?: boolean;
105
+ /** Thread ID — used by session-aware providers (e.g. ClaudeCode) to resume sessions. */
106
+ threadId?: string;
107
+ /** Working directory for this completion (worktree path). Provider uses its own default if omitted. */
108
+ cwd?: string;
109
+ /** Scheduling hint for providers that queue calls: higher runs first. Default 0. Others ignore it. */
110
+ priority?: number;
111
+ }
112
+
113
+ export interface CompletionResult {
114
+ readonly native?: import("./native-evidence").NativeEvidence;
115
+ readonly content: string;
116
+ readonly model: string;
117
+ readonly tokens?: TokenCounts;
118
+ readonly finishReason?: string;
119
+ /** Tool calls requested by the LLM (present when finishReason is "tool_use"). */
120
+ readonly toolCalls?: ToolCall[];
121
+ readonly raw?: unknown;
122
+ }
123
+
124
+ export interface EmbeddingResult {
125
+ readonly embedding: number[];
126
+ readonly tokens?: number;
127
+ }
128
+
129
+ export interface LLMStreamEvent {
130
+ type: "text" | "usage" | "done" | "error";
131
+ text?: string;
132
+ tokens?: TokenCounts;
133
+ error?: string;
134
+ finishReason?: string;
135
+ }
136
+
137
+ export interface LLMProvider {
138
+ readonly id: string;
139
+ /** Native callback support, distinct from stateless/legacy serializable options. */
140
+ readonly nativeOwnership?: "required-prewrite";
141
+ /** Optional owned-call evidence. A rejected waiter alone never proves remote settlement. */
142
+ readonly completionLifecycle?: {
143
+ readonly kind: "request" | "session";
144
+ settlement(outcome: { result?: CompletionResult; error?: unknown }): "settled" | "unknown";
145
+ admission?(outcome: { result?: CompletionResult; error?: unknown }): "not-admitted" | "attempted" | "unknown";
146
+ /** Read only the original registered admission. Never sends, resumes or replaces work. */
147
+ inspectOwnedAdmission?(owner: import("./native-evidence").NativeOwner, admissionId: string): Promise<import("./native-evidence").OwnedAdmissionInspection | undefined>;
148
+ /** Cleanup of that original, confirmed idle pool entry; never a current-scope lookup. */
149
+ releaseOwnedAdmission?(owner: import("./native-evidence").NativeOwner, admissionId: string): Promise<"released" | "unknown" | "unavailable">;
150
+ /** Release only a confirmed idle owned resource; never clear its resume binding. */
151
+ releaseIdle?(opts: CompletionOpts): Promise<"released" | "unknown" | "unavailable">;
152
+ };
153
+ complete(
154
+ messages: LLMMessage[],
155
+ opts?: CompletionOpts
156
+ ): Promise<CompletionResult>;
157
+ stream?(
158
+ messages: LLMMessage[],
159
+ opts?: CompletionOpts
160
+ ): AsyncGenerator<LLMStreamEvent>;
161
+ }
162
+
163
+ export interface EmbeddingProvider {
164
+ readonly id: string;
165
+ embed(text: string): Promise<EmbeddingResult>;
166
+ embedBatch?(texts: string[]): Promise<EmbeddingResult[]>;
167
+ }