@inixiative/foundry-core 0.5.9 → 0.5.11
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/package.json +4 -4
- package/src/action-prompt.ts +11 -2
- package/src/adapters/file-memory.ts +5 -0
- package/src/adapters/http-memory.ts +5 -0
- package/src/adapters/markdown-docs.ts +12 -1
- package/src/adapters/markdown-hierarchy.ts +7 -2
- package/src/adapters/sqlite-memory.ts +21 -6
- package/src/base-agent.ts +5 -0
- package/src/bounded-set.ts +5 -0
- package/src/cache-lifecycle.ts +5 -0
- package/src/capability.ts +5 -0
- package/src/clarifier.ts +5 -0
- package/src/classifier.ts +5 -0
- package/src/context-layer.ts +10 -0
- package/src/context-stack.ts +5 -0
- package/src/credentials.ts +5 -0
- package/src/decider.ts +5 -0
- package/src/delivery-evidence.ts +5 -0
- package/src/event-stream.ts +5 -0
- package/src/executor.ts +17 -4
- package/src/feedback/archive-turns.ts +5 -0
- package/src/feedback/correction.ts +48 -2
- package/src/feedback/experiment-gate.ts +5 -0
- package/src/feedback/review-findings.ts +5 -0
- package/src/feedback/warden-labels.ts +5 -0
- package/src/feedback/warden-prefilter.ts +5 -0
- package/src/feedback/warden-probes.ts +5 -0
- package/src/harness.ts +7 -2
- package/src/hydrator.ts +5 -0
- package/src/id.ts +5 -0
- package/src/index.ts +25 -12
- package/src/intervention.ts +5 -0
- package/src/messages.ts +5 -0
- package/src/middleware.ts +5 -0
- package/src/model-registry.ts +92 -2
- package/src/native-evidence.ts +18 -0
- package/src/native-tools.ts +5 -0
- package/src/retry.ts +5 -0
- package/src/router.ts +5 -0
- package/src/scope.ts +5 -0
- package/src/serialized-queue.ts +35 -0
- package/src/session-delivery.ts +5 -0
- package/src/signal.ts +5 -0
- package/src/thread.ts +196 -21
- package/src/token-counts.ts +5 -0
- package/src/token-tracker.ts +49 -24
- package/src/tools.ts +6 -1
- package/src/trace.ts +5 -0
- package/src/types.ts +5 -2
- package/src/hooks.ts +0 -133
package/src/thread.ts
CHANGED
|
@@ -1,3 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @atlas
|
|
3
|
+
* @kind service
|
|
4
|
+
* @partOf primitive:thread
|
|
5
|
+
*/
|
|
1
6
|
import type { BaseAgent, ExecutionResult } from './base-agent';
|
|
2
7
|
import { CacheLifecycle } from './cache-lifecycle';
|
|
3
8
|
import type { ContextStack, ContextStackView, LayerFilter } from './context-stack';
|
|
@@ -14,6 +19,8 @@ export type ThreadStatus = 'idle' | 'active' | 'waiting' | 'archived';
|
|
|
14
19
|
export interface ThreadName {
|
|
15
20
|
readonly text: string;
|
|
16
21
|
readonly updatedAt: number;
|
|
22
|
+
/** A Foundry name the thread's agent gave (`rename_thread`): Foundry's namer leaves it alone. */
|
|
23
|
+
readonly by?: 'agent';
|
|
17
24
|
}
|
|
18
25
|
|
|
19
26
|
/** Tracked work a thread links to, in Archive's integration-reference shape (`github` `owner/repo#12`, `linear` `ENG-4`). */
|
|
@@ -23,6 +30,132 @@ export interface ThreadReference {
|
|
|
23
30
|
readonly url?: string;
|
|
24
31
|
}
|
|
25
32
|
|
|
33
|
+
/**
|
|
34
|
+
* A thread's worktree record. `mode` is how its repository makes worktrees (`template`: slots via
|
|
35
|
+
* `worktree:create`; `classic`: plain git; `none`: the thread works outside any git checkout).
|
|
36
|
+
* `released` means the recorded checkout no longer exists on disk; the rest is what it was.
|
|
37
|
+
*/
|
|
38
|
+
export interface ThreadCheckout {
|
|
39
|
+
readonly mode: 'template' | 'classic' | 'none';
|
|
40
|
+
/** Worktree root (absent for `none`). */
|
|
41
|
+
readonly path?: string;
|
|
42
|
+
/** The repository's main checkout (absent for `none`). */
|
|
43
|
+
readonly root?: string;
|
|
44
|
+
/** The repository's main checkout rather than a linked worktree. */
|
|
45
|
+
readonly main: boolean;
|
|
46
|
+
/** Template mode: the slot the worktree holds (main checkout = 0). */
|
|
47
|
+
readonly slot?: number;
|
|
48
|
+
/** The branch was squash-merged into the repository's default branch (tree equality), and when it was first seen. */
|
|
49
|
+
readonly merged?: { readonly into: string; readonly at: number };
|
|
50
|
+
/** `recreating`: the mode's create is running for a released worktree. */
|
|
51
|
+
readonly state: 'live' | 'released' | 'recreating';
|
|
52
|
+
/** The last recreate failed: the command's error. */
|
|
53
|
+
readonly error?: string;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** What a thread finished: a merge of its branch, or a plan outcome marked done. */
|
|
57
|
+
export interface ThreadDone {
|
|
58
|
+
readonly kind: 'merged' | 'milestone';
|
|
59
|
+
readonly text: string;
|
|
60
|
+
readonly at: number;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** One item of a plan section: a short line, and its detail when the line is not enough. */
|
|
64
|
+
export interface ThreadPlanItem {
|
|
65
|
+
readonly text: string;
|
|
66
|
+
readonly detail?: string;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* The thread's plan, written by its agent through Foundry's `set_plan` tool. Outcomes are what the
|
|
71
|
+
* person gets (`you`: waits on the person); pains what hurts today; strategy how the work goes;
|
|
72
|
+
* reasons why this way. Each technical row names, per concern (auth, permissions, db access,
|
|
73
|
+
* requests, locks, jobs, events, errors…), the existing system it uses and where, or why none fits.
|
|
74
|
+
*/
|
|
75
|
+
export interface ThreadPlan {
|
|
76
|
+
readonly outcomes: readonly (ThreadPlanItem & {
|
|
77
|
+
readonly status: 'todo' | 'doing' | 'done' | 'you';
|
|
78
|
+
})[];
|
|
79
|
+
readonly pains: readonly ThreadPlanItem[];
|
|
80
|
+
readonly strategy: readonly ThreadPlanItem[];
|
|
81
|
+
readonly reasons: readonly ThreadPlanItem[];
|
|
82
|
+
readonly technical: readonly {
|
|
83
|
+
readonly concern: string;
|
|
84
|
+
readonly decision: 'reuse' | 'extend' | 'new' | 'combine' | 'refactor' | 'migrate' | 'none';
|
|
85
|
+
readonly system: string;
|
|
86
|
+
readonly path?: string;
|
|
87
|
+
readonly reason: string;
|
|
88
|
+
}[];
|
|
89
|
+
/**
|
|
90
|
+
* Who wrote it: the thread's agent (`set_plan`), or Foundry's draft when a turn that brought new
|
|
91
|
+
* work ended with no plan recorded. A draft is shown as one and the agent's next `set_plan`
|
|
92
|
+
* replaces it whole.
|
|
93
|
+
*/
|
|
94
|
+
readonly draftedBy?: 'agent' | 'foundry';
|
|
95
|
+
readonly updatedAt: number;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** One round of a thread's loop: how it went and what it found. */
|
|
99
|
+
export interface ThreadLoopRound {
|
|
100
|
+
readonly round: number;
|
|
101
|
+
readonly result: 'pass' | 'fail' | 'partial';
|
|
102
|
+
readonly note: string;
|
|
103
|
+
readonly at: number;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* The thread's loop: the process the agent repeats to reach the plan's outcomes (Aron, 2026-11-01:
|
|
108
|
+
* "The plan is the goals that you want to achieve. The loop is the process you're doing to achieve
|
|
109
|
+
* them"). Recorded by the agent (`set_loop`, `loop_round`, `stop_loop`) for work that needs several
|
|
110
|
+
* rounds of build, check and fix; shown in the Loop tab.
|
|
111
|
+
*/
|
|
112
|
+
export interface ThreadLoop {
|
|
113
|
+
/** What the loop drives toward, in a line. */
|
|
114
|
+
readonly goal: string;
|
|
115
|
+
/** One round, in order: e.g. build, run the check, fix what failed. */
|
|
116
|
+
readonly steps: readonly ThreadPlanItem[];
|
|
117
|
+
/** When it stops: the check that passing means done. */
|
|
118
|
+
readonly until: string;
|
|
119
|
+
/** Rounds at most before the agent stops and asks. */
|
|
120
|
+
readonly maxRounds?: number;
|
|
121
|
+
readonly rounds: readonly ThreadLoopRound[];
|
|
122
|
+
/** `done`: the stop condition held. `stopped`: ended without it (the reason says why). */
|
|
123
|
+
readonly state: 'running' | 'done' | 'stopped';
|
|
124
|
+
readonly stopReason?: string;
|
|
125
|
+
readonly startedAt: number;
|
|
126
|
+
readonly updatedAt: number;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* What the thread's agent says about where it stands, when Foundry can't see it: it waits on the
|
|
131
|
+
* person (an orange "the agent asks"), or it is blocked (red). Working, queued, failed, approvals
|
|
132
|
+
* and archived are Foundry's to know and never set here; done is `ThreadMeta.done`. Cleared by the
|
|
133
|
+
* owner's next message.
|
|
134
|
+
*/
|
|
135
|
+
export interface ThreadAgentStatus {
|
|
136
|
+
readonly state: 'needs-you' | 'blocked';
|
|
137
|
+
readonly text: string;
|
|
138
|
+
readonly at: number;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/** Where a thread runs: the Foundry device it was recorded on, and that Foundry's name then. */
|
|
142
|
+
export interface ThreadMachine {
|
|
143
|
+
readonly deviceId: string;
|
|
144
|
+
readonly deviceName: string;
|
|
145
|
+
readonly installationName?: string;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* A thread snoozed by its person: quiet until `until` (ISO), then cleared. `condition` names what
|
|
150
|
+
* the time was resolved from (`quota-reset`: when the reached usage window resets). A snooze never
|
|
151
|
+
* changes the thread's status, so clearing it leaves the thread as it was.
|
|
152
|
+
*/
|
|
153
|
+
export interface ThreadSnooze {
|
|
154
|
+
readonly until: string;
|
|
155
|
+
readonly condition?: 'quota-reset';
|
|
156
|
+
readonly setAt: string;
|
|
157
|
+
}
|
|
158
|
+
|
|
26
159
|
/** Where a thread's work lives, as the runtime last observed it. */
|
|
27
160
|
export interface ThreadContext {
|
|
28
161
|
/** `owner/repo` of the worktree's origin. */
|
|
@@ -31,19 +164,36 @@ export interface ThreadContext {
|
|
|
31
164
|
readonly branch?: string;
|
|
32
165
|
/** Worktree root containing the thread's cwd. */
|
|
33
166
|
readonly worktree?: string;
|
|
167
|
+
/** The thread's checkout as a worktree record (FEAT-003): mode, slot and whether it still exists. */
|
|
168
|
+
readonly checkout?: ThreadCheckout;
|
|
34
169
|
readonly references: readonly ThreadReference[];
|
|
35
170
|
readonly updatedAt: number;
|
|
36
171
|
}
|
|
37
172
|
|
|
38
|
-
/** The name a thread shows: a person's name overrides the
|
|
173
|
+
/** The name a thread shows: a person's name overrides the one Foundry gives it. */
|
|
39
174
|
export function threadTitle(
|
|
40
|
-
meta: Pick<ThreadMeta, 'name' | '
|
|
41
|
-
): { text: string; source: 'human' | 'agent' | 'description' } | undefined {
|
|
175
|
+
meta: Pick<ThreadMeta, 'name' | 'foundryName' | 'description'>,
|
|
176
|
+
): { text: string; source: 'human' | 'agent' | 'foundry' | 'description' } | undefined {
|
|
42
177
|
if (meta.name?.text) return { text: meta.name.text, source: 'human' };
|
|
43
|
-
if (meta.
|
|
178
|
+
if (meta.foundryName?.text)
|
|
179
|
+
return {
|
|
180
|
+
text: meta.foundryName.text,
|
|
181
|
+
source: meta.foundryName.by === 'agent' ? 'agent' : 'foundry',
|
|
182
|
+
};
|
|
44
183
|
return meta.description ? { text: meta.description, source: 'description' } : undefined;
|
|
45
184
|
}
|
|
46
185
|
|
|
186
|
+
/**
|
|
187
|
+
* Where a forked thread came from: provenance, not nesting. A fork is a top-level thread (Aron,
|
|
188
|
+
* 2026-11-01: delegated threads are top-level; nesting is for subagents), so it is not a
|
|
189
|
+
* `parentThreadId`. Its first session is seeded with the source's conversation through `journalSeq`.
|
|
190
|
+
*/
|
|
191
|
+
export interface ThreadFork {
|
|
192
|
+
readonly threadId: string;
|
|
193
|
+
/** The source's journal seq the fork was cut at. */
|
|
194
|
+
readonly journalSeq: number;
|
|
195
|
+
}
|
|
196
|
+
|
|
47
197
|
/** How a subagent thread came to be: who spawned it, through what, and its native identity. */
|
|
48
198
|
export interface ThreadSpawn {
|
|
49
199
|
readonly via: 'foundry' | 'native-claude' | 'native-codex' | 'host-paseo' | 'host-t3';
|
|
@@ -66,8 +216,11 @@ export interface ThreadMeta {
|
|
|
66
216
|
/** Name a person set. A hard override: agents never change it. */
|
|
67
217
|
name?: ThreadName;
|
|
68
218
|
|
|
69
|
-
/**
|
|
70
|
-
|
|
219
|
+
/**
|
|
220
|
+
* Name Foundry gives the thread from the person's messages (its first message, then again when
|
|
221
|
+
* the work moves) while no person has named it. Never set once a person has.
|
|
222
|
+
*/
|
|
223
|
+
foundryName?: ThreadName;
|
|
71
224
|
|
|
72
225
|
/** Branch, worktree and linked PRs/tickets, derived by the runtime. */
|
|
73
226
|
context?: ThreadContext;
|
|
@@ -78,6 +231,30 @@ export interface ThreadMeta {
|
|
|
78
231
|
/** Current status. */
|
|
79
232
|
status: ThreadStatus;
|
|
80
233
|
|
|
234
|
+
/**
|
|
235
|
+
* The thread's last finished piece of work, shown as its done state until the next task starts:
|
|
236
|
+
* its branch merged (`merged`), or a plan outcome reached `done` (`milestone`, with its text).
|
|
237
|
+
*/
|
|
238
|
+
done?: ThreadDone;
|
|
239
|
+
|
|
240
|
+
/** The agent's plan for the thread's work (`set_plan`, the Plan view): what, why, and how. */
|
|
241
|
+
plan?: ThreadPlan;
|
|
242
|
+
/** The process the agent repeats to reach the plan (`set_loop`, the Loop view). */
|
|
243
|
+
loop?: ThreadLoop;
|
|
244
|
+
/** Where the agent says it stands: waiting on the person or blocked (`set_status`). */
|
|
245
|
+
agentStatus?: ThreadAgentStatus;
|
|
246
|
+
/** The machine (Foundry device identity, `~/.foundry/device.json`) the thread runs on. */
|
|
247
|
+
machine?: ThreadMachine;
|
|
248
|
+
|
|
249
|
+
/** Put out of mind until a time (or a condition, resolved to its time); status is untouched. */
|
|
250
|
+
snooze?: ThreadSnooze;
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* When the owner last looked at the thread (epoch ms; the viewer marks it on open and while open).
|
|
254
|
+
* A turn that completed after it is a hand-back, a "new arrival" (VIEWER-001's seen-by-Aron mark).
|
|
255
|
+
*/
|
|
256
|
+
seenAt?: number;
|
|
257
|
+
|
|
81
258
|
/** Working directory for this thread's tool execution (worktree path). */
|
|
82
259
|
cwd?: string;
|
|
83
260
|
|
|
@@ -90,21 +267,21 @@ export interface ThreadMeta {
|
|
|
90
267
|
/** The parent's turn that spawned this thread (a subagent). */
|
|
91
268
|
parentTurnId?: string;
|
|
92
269
|
|
|
270
|
+
/** The thread and point this one was forked from (provenance; a fork is top-level). */
|
|
271
|
+
forkedFrom?: ThreadFork;
|
|
272
|
+
|
|
93
273
|
/** How this subagent thread was spawned. */
|
|
94
274
|
spawn?: ThreadSpawn;
|
|
95
275
|
|
|
96
276
|
/** 1 for a child of a main thread. */
|
|
97
277
|
depth?: number;
|
|
98
278
|
|
|
99
|
-
/**
|
|
100
|
-
* Start-up of a thread whose Wardens are held in its own process (docs/THREAD-TREE.md spec C):
|
|
101
|
-
* turns typed meanwhile wait as `queued` until it is `ready` (or `degraded`).
|
|
102
|
-
*/
|
|
103
|
-
setup?: ThreadSetup;
|
|
104
|
-
|
|
105
279
|
/** Project that owns this thread. Scopes memory sources and tool calls. */
|
|
106
280
|
projectId?: string;
|
|
107
281
|
|
|
282
|
+
/** The harness running this thread's main session (`claude-code`, `codex`). Set at creation; never changed silently. */
|
|
283
|
+
worker?: string;
|
|
284
|
+
|
|
108
285
|
/** When this thread was created. */
|
|
109
286
|
readonly createdAt: number;
|
|
110
287
|
|
|
@@ -115,15 +292,6 @@ export interface ThreadMeta {
|
|
|
115
292
|
archivedAt?: number;
|
|
116
293
|
}
|
|
117
294
|
|
|
118
|
-
export interface ThreadSetup {
|
|
119
|
-
status: 'starting' | 'ready' | 'degraded';
|
|
120
|
-
startedAt: number;
|
|
121
|
-
readyAt?: number;
|
|
122
|
-
/** Every configured Warden, those holding a spare, and those that did not by the deadline. */
|
|
123
|
-
wardens: { expected: number; held: number; failed: string[] };
|
|
124
|
-
runtime: 'claude-in-session' | 'codex-in-process' | 'side-session';
|
|
125
|
-
}
|
|
126
|
-
|
|
127
295
|
export interface Dispatch<T = unknown> {
|
|
128
296
|
readonly agentId: string;
|
|
129
297
|
readonly timestamp: number;
|
|
@@ -214,8 +382,12 @@ export interface ThreadConfig {
|
|
|
214
382
|
branch?: string;
|
|
215
383
|
/** Parent thread when this thread is a subagent spawned by another. */
|
|
216
384
|
parentThreadId?: string;
|
|
385
|
+
/** The thread and point this one was forked from. */
|
|
386
|
+
forkedFrom?: ThreadFork;
|
|
217
387
|
/** Project that owns this thread. Scopes memory sources and tool calls. */
|
|
218
388
|
projectId?: string;
|
|
389
|
+
/** The harness running this thread's main session. */
|
|
390
|
+
worker?: string;
|
|
219
391
|
/** Optional token tracker — auto-records costs for every dispatch. */
|
|
220
392
|
tokenTracker?: TokenTracker;
|
|
221
393
|
}
|
|
@@ -266,7 +438,9 @@ export class Thread {
|
|
|
266
438
|
cwd: opts?.cwd,
|
|
267
439
|
branch: opts?.branch,
|
|
268
440
|
parentThreadId: opts?.parentThreadId,
|
|
441
|
+
...(opts?.forkedFrom ? { forkedFrom: opts.forkedFrom } : {}),
|
|
269
442
|
projectId: opts?.projectId,
|
|
443
|
+
...(opts?.worker ? { worker: opts.worker } : {}),
|
|
270
444
|
createdAt: now,
|
|
271
445
|
lastActiveAt: now,
|
|
272
446
|
};
|
|
@@ -450,6 +624,7 @@ export class Thread {
|
|
|
450
624
|
register: opts.nativeObservation.register,
|
|
451
625
|
observe: opts.nativeObservation.observe,
|
|
452
626
|
interruptible: opts.nativeObservation.interruptible,
|
|
627
|
+
steerable: opts.nativeObservation.steerable,
|
|
453
628
|
thought: opts.nativeObservation.thought,
|
|
454
629
|
owner: Object.freeze({
|
|
455
630
|
threadId: this.id,
|
package/src/token-counts.ts
CHANGED
|
@@ -1,3 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @atlas
|
|
3
|
+
* @kind helper
|
|
4
|
+
* @partOf feature:usage
|
|
5
|
+
*/
|
|
1
6
|
/** Cache counts, when supplied, are disjoint from input. Thinking and TTL
|
|
2
7
|
* breakdowns are subsets of output/cacheWrite, never additional tokens.
|
|
3
8
|
* Missing optional counters mean unreported, not a measured zero.
|
package/src/token-tracker.ts
CHANGED
|
@@ -1,3 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @atlas
|
|
3
|
+
* @kind service
|
|
4
|
+
* @partOf feature:usage
|
|
5
|
+
*/
|
|
1
6
|
import { sumTokenCounts, type TokenCounts, totalTokenCount } from './token-counts';
|
|
2
7
|
|
|
3
8
|
// ---------------------------------------------------------------------------
|
|
@@ -140,6 +145,10 @@ const attributionMatches = (
|
|
|
140
145
|
export interface ModelPricing {
|
|
141
146
|
readonly inputPer1M: number;
|
|
142
147
|
readonly outputPer1M: number;
|
|
148
|
+
/** Cache reads; default a tenth of input. */
|
|
149
|
+
readonly cacheReadPer1M?: number;
|
|
150
|
+
/** Cache writes (5-minute); default 1.25x input. */
|
|
151
|
+
readonly cacheWritePer1M?: number;
|
|
143
152
|
}
|
|
144
153
|
|
|
145
154
|
/**
|
|
@@ -209,11 +218,22 @@ export interface UsageSummary {
|
|
|
209
218
|
}
|
|
210
219
|
|
|
211
220
|
// ---------------------------------------------------------------------------
|
|
212
|
-
// Default cost table
|
|
221
|
+
// Default cost table. Anthropic: first-party API list prices per 1M tokens, as published at
|
|
222
|
+
// https://platform.claude.com/docs/en/about-claude/pricing (read 2026-11-01; cache writes are the
|
|
223
|
+
// 5-minute rate, 1.25x input). Haiku 5.5 is its rate up to 100K-token prompts.
|
|
213
224
|
// ---------------------------------------------------------------------------
|
|
214
225
|
|
|
215
226
|
export const DEFAULT_COST_TABLE: CostTable = {
|
|
216
227
|
anthropic: {
|
|
228
|
+
'claude-fable-5-1': { inputPer1M: 10, outputPer1M: 50, cacheReadPer1M: 0.25 },
|
|
229
|
+
'claude-fable-5': { inputPer1M: 10, outputPer1M: 50 },
|
|
230
|
+
'claude-opus-5-5': { inputPer1M: 4, outputPer1M: 20, cacheReadPer1M: 0.2 },
|
|
231
|
+
'claude-opus-5': { inputPer1M: 5, outputPer1M: 25 },
|
|
232
|
+
'claude-opus-4-8': { inputPer1M: 5, outputPer1M: 25 },
|
|
233
|
+
'claude-sonnet-5-5': { inputPer1M: 2, outputPer1M: 10, cacheReadPer1M: 0.2 },
|
|
234
|
+
'claude-sonnet-5': { inputPer1M: 2, outputPer1M: 10 },
|
|
235
|
+
'claude-haiku-5-5': { inputPer1M: 0.1, outputPer1M: 0.5 },
|
|
236
|
+
'claude-haiku-4-5': { inputPer1M: 1, outputPer1M: 5 },
|
|
217
237
|
'claude-opus-4-7': { inputPer1M: 5, outputPer1M: 25 },
|
|
218
238
|
'claude-opus-4-6': { inputPer1M: 5, outputPer1M: 25 },
|
|
219
239
|
'claude-sonnet-4-6': { inputPer1M: 3, outputPer1M: 15 },
|
|
@@ -239,10 +259,24 @@ export const DEFAULT_COST_TABLE: CostTable = {
|
|
|
239
259
|
'muse-spark-1.2-contributor': { inputPer1M: 0.1, outputPer1M: 0.2 },
|
|
240
260
|
'muse-spark-1.1': { inputPer1M: 1.25, outputPer1M: 4.25 },
|
|
241
261
|
},
|
|
242
|
-
// Claude Code provider tracks usage via session output, not cost table
|
|
243
|
-
'claude-code': {},
|
|
244
262
|
};
|
|
245
263
|
|
|
264
|
+
/**
|
|
265
|
+
* A model's price: its own provider's row, else the same model id under any provider. A Claude
|
|
266
|
+
* model run through the Claude Code login (provider `claude-code` or a decision host) is priced at
|
|
267
|
+
* its API list price: an API-equivalent cost, not what a subscription bills.
|
|
268
|
+
*/
|
|
269
|
+
export function modelPricing(
|
|
270
|
+
table: CostTable,
|
|
271
|
+
provider: string,
|
|
272
|
+
model: string,
|
|
273
|
+
): ModelPricing | undefined {
|
|
274
|
+
const own = table[provider]?.[model];
|
|
275
|
+
if (own) return own;
|
|
276
|
+
for (const rows of Object.values(table)) if (rows[model]) return rows[model];
|
|
277
|
+
return undefined;
|
|
278
|
+
}
|
|
279
|
+
|
|
246
280
|
// ---------------------------------------------------------------------------
|
|
247
281
|
// Token estimation
|
|
248
282
|
// ---------------------------------------------------------------------------
|
|
@@ -316,19 +350,14 @@ export class TokenTracker {
|
|
|
316
350
|
* Throws if budget.haltAt is exceeded (when configured).
|
|
317
351
|
*/
|
|
318
352
|
record(entry: Omit<UsageEntry, 'timestamp' | 'cost'>): UsageEntry {
|
|
319
|
-
const
|
|
320
|
-
|
|
321
|
-
entry.model,
|
|
322
|
-
entry.tokens.input,
|
|
323
|
-
entry.tokens.output,
|
|
324
|
-
entry.cached,
|
|
325
|
-
);
|
|
353
|
+
const pricing = modelPricing(this._costTable, entry.provider, entry.model);
|
|
354
|
+
const cost = pricing ? this._computeCost(pricing, entry.tokens, entry.cached) : 0;
|
|
326
355
|
|
|
327
356
|
const full: UsageEntry = {
|
|
328
357
|
...entry,
|
|
329
358
|
timestamp: Date.now(),
|
|
330
359
|
cost,
|
|
331
|
-
costKnown: !!
|
|
360
|
+
costKnown: !!pricing,
|
|
332
361
|
};
|
|
333
362
|
|
|
334
363
|
this._entries.push(full);
|
|
@@ -474,20 +503,16 @@ export class TokenTracker {
|
|
|
474
503
|
// Private helpers
|
|
475
504
|
// -----------------------------------------------------------------------
|
|
476
505
|
|
|
477
|
-
private _computeCost(
|
|
478
|
-
provider
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
cached?: boolean,
|
|
483
|
-
): number {
|
|
484
|
-
const pricing = this._costTable[provider]?.[model];
|
|
485
|
-
if (!pricing) return 0;
|
|
486
|
-
|
|
487
|
-
// Cached input tokens are typically free or heavily discounted
|
|
488
|
-
const effectiveInput = cached ? input * 0.1 : input;
|
|
506
|
+
private _computeCost(pricing: ModelPricing, tokens: TokenCounts, cached?: boolean): number {
|
|
507
|
+
// `cached` marks a whole call served from a provider cache that reports no split.
|
|
508
|
+
const input = cached ? tokens.input * 0.1 : tokens.input;
|
|
509
|
+
const perToken = (count: number | undefined, per1M: number) =>
|
|
510
|
+
((count ?? 0) / 1_000_000) * per1M;
|
|
489
511
|
return (
|
|
490
|
-
(
|
|
512
|
+
perToken(input, pricing.inputPer1M) +
|
|
513
|
+
perToken(tokens.output, pricing.outputPer1M) +
|
|
514
|
+
perToken(tokens.cacheRead, pricing.cacheReadPer1M ?? pricing.inputPer1M * 0.1) +
|
|
515
|
+
perToken(tokens.cacheWrite, pricing.cacheWritePer1M ?? pricing.inputPer1M * 1.25)
|
|
491
516
|
);
|
|
492
517
|
}
|
|
493
518
|
|
package/src/tools.ts
CHANGED
|
@@ -1,3 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @atlas
|
|
3
|
+
* @kind type
|
|
4
|
+
* @partOf primitive:tools
|
|
5
|
+
*/
|
|
1
6
|
// ---------------------------------------------------------------------------
|
|
2
7
|
// Tools — typed interfaces for agent execution environments
|
|
3
8
|
// ---------------------------------------------------------------------------
|
|
@@ -721,7 +726,7 @@ export class ToolRegistry {
|
|
|
721
726
|
const api = tool.scoped ? tool.scoped(opts?.scope ?? {}) : tool;
|
|
722
727
|
return await api.request({
|
|
723
728
|
url: input.url as string,
|
|
724
|
-
method: (input.method as
|
|
729
|
+
method: (input.method as ApiRequest['method']) ?? 'GET',
|
|
725
730
|
body: input.body,
|
|
726
731
|
headers: input.headers as Record<string, string>,
|
|
727
732
|
});
|
package/src/trace.ts
CHANGED
package/src/types.ts
CHANGED
|
@@ -1,3 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @atlas
|
|
3
|
+
* @kind type
|
|
4
|
+
* @partOf primitive:agents
|
|
5
|
+
*/
|
|
1
6
|
import type { TokenCounts } from './token-counts';
|
|
2
7
|
// ---------------------------------------------------------------------------
|
|
3
8
|
// Shared types — used by harness for conditional invocation/activation
|
|
@@ -212,8 +217,6 @@ export interface LLMProvider {
|
|
|
212
217
|
* `threadPrefix` starts at once. `messages` end at those prefixes; no decision is made. Optional.
|
|
213
218
|
*/
|
|
214
219
|
prestart?(messages: LLMMessage[], opts: CompletionOpts): void;
|
|
215
|
-
/** A thread closed: release whatever the provider holds for it. Optional. */
|
|
216
|
-
releaseThread?(threadId: string): Promise<void>;
|
|
217
220
|
}
|
|
218
221
|
|
|
219
222
|
export interface EmbeddingProvider {
|
package/src/hooks.ts
DELETED
|
@@ -1,133 +0,0 @@
|
|
|
1
|
-
// ---------------------------------------------------------------------------
|
|
2
|
-
// Types
|
|
3
|
-
// ---------------------------------------------------------------------------
|
|
4
|
-
|
|
5
|
-
export type HookPoint =
|
|
6
|
-
| 'pre:dispatch'
|
|
7
|
-
| 'post:dispatch'
|
|
8
|
-
| 'pre:classify'
|
|
9
|
-
| 'post:classify'
|
|
10
|
-
| 'pre:route'
|
|
11
|
-
| 'post:route'
|
|
12
|
-
| 'pre:compact'
|
|
13
|
-
| 'post:compact'
|
|
14
|
-
| 'session:create'
|
|
15
|
-
| 'session:archive'
|
|
16
|
-
| 'budget:warning'
|
|
17
|
-
| 'budget:exceeded'
|
|
18
|
-
| 'plan:enter'
|
|
19
|
-
| 'plan:exit'
|
|
20
|
-
| 'error:agent'
|
|
21
|
-
| 'error:provider';
|
|
22
|
-
|
|
23
|
-
export interface HookContext {
|
|
24
|
-
hookPoint: HookPoint;
|
|
25
|
-
threadId?: string;
|
|
26
|
-
agentId?: string;
|
|
27
|
-
payload?: unknown;
|
|
28
|
-
result?: unknown;
|
|
29
|
-
meta: Record<string, unknown>;
|
|
30
|
-
timestamp: number;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
export interface HookResult {
|
|
34
|
-
action: 'continue' | 'skip' | 'abort' | 'redirect';
|
|
35
|
-
redirectTo?: string;
|
|
36
|
-
modified?: Partial<HookContext>;
|
|
37
|
-
annotations?: Record<string, unknown>;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
export interface HookHandler {
|
|
41
|
-
id: string;
|
|
42
|
-
points: HookPoint[];
|
|
43
|
-
priority?: number;
|
|
44
|
-
handler: (ctx: HookContext) => Promise<HookResult>;
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
export interface PlanModeTrigger {
|
|
48
|
-
kind: 'complexity' | 'newDomain' | 'largeDiff' | 'custom';
|
|
49
|
-
threshold?: number;
|
|
50
|
-
detect?: (ctx: HookContext) => boolean;
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
export interface PlanModeConfig {
|
|
54
|
-
triggers: PlanModeTrigger[];
|
|
55
|
-
plannerAgentId: string;
|
|
56
|
-
requireApproval?: boolean;
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
// ---------------------------------------------------------------------------
|
|
60
|
-
// HookRegistry — engine mechanism, no built-in behaviors
|
|
61
|
-
// ---------------------------------------------------------------------------
|
|
62
|
-
|
|
63
|
-
const DEFAULT_PRIORITY = 100;
|
|
64
|
-
|
|
65
|
-
export class HookRegistry {
|
|
66
|
-
private _handlers: Map<string, HookHandler> = new Map();
|
|
67
|
-
|
|
68
|
-
register(handler: HookHandler): () => void {
|
|
69
|
-
this._handlers.set(handler.id, handler);
|
|
70
|
-
return () => {
|
|
71
|
-
this._handlers.delete(handler.id);
|
|
72
|
-
};
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
unregister(id: string): boolean {
|
|
76
|
-
return this._handlers.delete(id);
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
async execute(
|
|
80
|
-
point: HookPoint,
|
|
81
|
-
ctx: HookContext,
|
|
82
|
-
): Promise<HookContext & { action: HookResult['action'] }> {
|
|
83
|
-
const handlers = this.forPoint(point);
|
|
84
|
-
let current = { ...ctx };
|
|
85
|
-
let action: HookResult['action'] = 'continue';
|
|
86
|
-
|
|
87
|
-
for (const handler of handlers) {
|
|
88
|
-
const result = await handler.handler(current);
|
|
89
|
-
action = result.action;
|
|
90
|
-
|
|
91
|
-
if (result.modified) {
|
|
92
|
-
current = {
|
|
93
|
-
...current,
|
|
94
|
-
...result.modified,
|
|
95
|
-
meta: { ...current.meta, ...result.modified.meta },
|
|
96
|
-
};
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
if (result.annotations) {
|
|
100
|
-
current.meta = { ...current.meta, ...result.annotations };
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
if (result.redirectTo) {
|
|
104
|
-
current.meta = { ...current.meta, redirectTo: result.redirectTo };
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
if (action === 'abort' || action === 'skip' || action === 'redirect') {
|
|
108
|
-
break;
|
|
109
|
-
}
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
return { ...current, action };
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
forPoint(point: HookPoint): ReadonlyArray<HookHandler> {
|
|
116
|
-
const matching: HookHandler[] = [];
|
|
117
|
-
for (const handler of this._handlers.values()) {
|
|
118
|
-
if (handler.points.includes(point)) {
|
|
119
|
-
matching.push(handler);
|
|
120
|
-
}
|
|
121
|
-
}
|
|
122
|
-
matching.sort((a, b) => (a.priority ?? DEFAULT_PRIORITY) - (b.priority ?? DEFAULT_PRIORITY));
|
|
123
|
-
return matching;
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
get size(): number {
|
|
127
|
-
return this._handlers.size;
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
get handlers(): ReadonlyArray<HookHandler> {
|
|
131
|
-
return [...this._handlers.values()];
|
|
132
|
-
}
|
|
133
|
-
}
|