tardie 0.3.0 → 0.4.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.
@@ -1,281 +0,0 @@
1
- import { Clock, Effect } from "effect"
2
- import type { KeyValueStore } from "effect/unstable/persistence"
3
- import { actor, transition, type Actor, type Reactor, type Self } from "tardie/core/actor"
4
- import { composeKeys, type KeyFragment } from "tardie/core/event-log"
5
- import { messageKeys } from "tardie/core/message"
6
- import type { Event } from "tardie/core/event"
7
- import { codeDispatched, codeKeys } from "tardie/code/events"
8
- import { codeReactorFor, type CodePolicy } from "tardie/code/execute"
9
- import type { Package, PackageRequirements } from "tardie/code/packages"
10
- import type { Router } from "tardie/core/router"
11
- import type { ToolSpec } from "./request"
12
- import { agentKeys, toolReturned } from "./events"
13
- import { inferReactorFor, type Infer, type InferPolicy } from "./infer"
14
- import { toolsReactorFrom, type Answer, type PendingCall, type Serve } from "./tools"
15
- import { budgetReactorFor, type BudgetPolicy } from "./budget"
16
- import { replyReactor } from "./reply"
17
- import { compactionReactorFor, type ContextPolicy } from "./compaction"
18
- import type { AgentR } from "./turn"
19
-
20
- // A Capability is one component of an agent: it provides the model its context and services
21
- // the calls that come back, as one value. `tools`, `system`, and `context` are the capability's
22
- // render into the model's request, re-derived from the log each attempt; `reactors` and `serve`
23
- // are its handlers. Mounting a capability is adding it to the agentOf list; there is no second
24
- // place to update.
25
- //
26
- // Every field but `name` is optional, because capabilities come in shapes: a policy capability
27
- // (reply, budget, compaction) has reactors with nothing to show; a tool-list capability has
28
- // tools and a serve with no lane of its own; code mode has all four.
29
- export interface Capability<R = never> {
30
- // The capability's name, for the collision error and span attributes.
31
- readonly name: string
32
- // The capability's alphabet fragment. agentOf composes fragments the way composeKeys does,
33
- // and a prefix collision is the same construction-time error.
34
- readonly keys?: KeyFragment
35
- // The reactors that settle this capability's work.
36
- readonly reactors?: ReadonlyArray<Reactor<R>>
37
- // tools derives what the model is shown, a projection of the log like any other: a constant
38
- // capability ignores its argument, a gated one filters by what has happened.
39
- // The composed tools are what the infer reactor hands the model
40
- // binding per attempt; nothing about tools lives in ModelConfig.
41
- readonly tools?: (log: ReadonlyArray<Event>) => ReadonlyArray<ToolSpec>
42
- // The system fragment explaining the tools, a projection of the log like `tools`: a constant
43
- // string is the constant projection, and a function derives the fragment from what has
44
- // happened. Fragments join in agentOf order.
45
- readonly system?: string | ((log: ReadonlyArray<Event>) => string)
46
- // What the render truncates and where. The capability that applies a context policy to its
47
- // reactor states the same one here, so the binding renders against the policy the guard
48
- // holds and the two cannot drift (compactionFor).
49
- readonly context?: Partial<ContextPolicy>
50
- // serve returns the transitions one call owes: an empty array while the world still works,
51
- // undefined when the call names no tool of this capability (agentOf asks the next capability,
52
- // and the reactor answers unknown-tool when every capability declines). The reactor owns the
53
- // key and the turn stamp through `answer`.
54
- readonly serve?: Serve<R>
55
- }
56
-
57
- // renderOf is the composed request half for one derivation: what the model would be shown over
58
- // this log, and the context policy the render obeys. The infer reactor is its one production
59
- // reader; tests read it directly. Context fields merge in mount order, last statement wins.
60
- export const renderOf = <R>(
61
- capabilities: ReadonlyArray<Capability<R>>,
62
- log: ReadonlyArray<Event>
63
- ): { readonly system: string; readonly tools: ReadonlyArray<ToolSpec>; readonly context: Partial<ContextPolicy> } => ({
64
- system: capabilities
65
- .map((c) => (typeof c.system === "function" ? c.system(log) : c.system))
66
- .filter((s): s is string => s !== undefined && s !== "")
67
- .join("\n"),
68
- tools: capabilities.flatMap((c) => c.tools?.(log) ?? []),
69
- context: Object.assign({}, ...capabilities.map((c) => c.context ?? {})) as Partial<ContextPolicy>
70
- })
71
-
72
- // RequirementsOf extracts one capability's R, so a mixed list infers the union of what its
73
- // members need rather than failing on the first element's R.
74
- type RequirementsOf<C> = C extends Capability<infer R> ? R : never
75
-
76
- // agentOf mounts capabilities into an actor. The infer and call-routing reactors are the
77
- // runtime of the component model: agentOf injects them, and they are never listed as
78
- // capabilities, the way a component tree does not list the renderer. The canonical inbound and
79
- // the agent alphabet are the runtime's own key table; capability fragments compose beside them,
80
- // and a prefix collision is composeKeys' construction-time error. Two capabilities deriving the
81
- // same tool name collide at construction too, checked over the empty log (a log-gated duplicate
82
- // still routes to the first capability that recognizes it).
83
- // The constraint is `Capability<unknown>` rather than `Capability<AgentR>`: a capability may need a
84
- // service the runtime knows nothing about (a file system, an HTTP client), and the union of those
85
- // needs is what the returned actor's type carries to the host that must bind them
86
- // (apps/server/src/host.ts). Narrowing is still refused, because R is covariant: a capability that
87
- // needs more cannot pass as one that needs less (capability.test.ts, "a mounted package's
88
- // requirements ride the capability's type").
89
- export const agentOf = <const Caps extends ReadonlyArray<Capability<never> | Capability<unknown>>>(
90
- caps: Caps,
91
- policy: Partial<InferPolicy> = {}
92
- ): Actor<AgentR | RequirementsOf<Caps[number]>> => {
93
- const capabilities = caps as ReadonlyArray<Capability<AgentR | RequirementsOf<Caps[number]>>>
94
- const named = new Map<string, string>()
95
- for (const c of capabilities) {
96
- for (const t of c.tools?.([]) ?? []) {
97
- const holder = named.get(t.name)
98
- if (holder !== undefined) throw new Error(`tool "${t.name}" declared by capabilities ${holder} and ${c.name}`)
99
- named.set(t.name, c.name)
100
- }
101
- }
102
- const serve = (call: PendingCall, log: ReadonlyArray<Event>, answer: Answer) => {
103
- for (const c of capabilities) {
104
- const served = c.serve?.(call, log, answer)
105
- if (served !== undefined) return served
106
- }
107
- return undefined
108
- }
109
- return actor<AgentR | RequirementsOf<Caps[number]>>(
110
- [
111
- inferReactorFor(policy, (log) => renderOf(capabilities, log)),
112
- toolsReactorFrom(serve, (log) => renderOf(capabilities, log).tools),
113
- ...capabilities.flatMap((c) => c.reactors ?? [])
114
- ],
115
- composeKeys(messageKeys, agentKeys, ...capabilities.flatMap((c) => (c.keys === undefined ? [] : [c.keys])))
116
- )
117
- }
118
-
119
- // EXECUTE_TOOL is code mode's one work tool: the model acts by writing JavaScript against the
120
- // packages in scope.
121
- const EXECUTE_TOOL: ToolSpec = {
122
- name: "execute",
123
- description:
124
- "Run JavaScript against the connected packages. Packages are objects in scope; await their methods and end with `return <value>`. The returned value comes back as this call's result, and console output comes back beside it as `logs` (capped; return the value you need, print to inspect).",
125
- inputSchema: {
126
- type: "object",
127
- properties: { code: { type: "string", description: "The JavaScript body to run." } },
128
- required: ["code"],
129
- additionalProperties: false
130
- }
131
- }
132
-
133
- // CODE_SYSTEM is code mode's system fragment with nothing in scope: it names the tool the model
134
- // acts with and says the scope is empty. codeSystemFor renders the same sentence over the
135
- // packages an assembly passed, so what the model reads is derived from the same values the code
136
- // reactor mounts (capability.test.ts, "a mounted package names itself in the system fragment").
137
- const CODE_SYSTEM_LEAD = "You act on the world by calling the execute tool with JavaScript; the packages in scope are:"
138
- export const CODE_SYSTEM = `${CODE_SYSTEM_LEAD}\nnone`
139
-
140
- // codeSystemFor names each package on its own line, `name: description`. An empty list reads
141
- // "none", which is CODE_SYSTEM.
142
- export const codeSystemFor = (packages: ReadonlyArray<Package<unknown>>): string =>
143
- `${CODE_SYSTEM_LEAD}\n${packages.length === 0 ? "none" : packages.map((p) => `${p.name}: ${p.description}`).join("\n")}`
144
-
145
- // settleFor reads one execution's outcome, once the code reactor has recorded it.
146
- const settleFor = (
147
- log: ReadonlyArray<Event>,
148
- callId: string
149
- ): { result?: unknown; error?: string; logs?: ReadonlyArray<string> } | undefined => {
150
- const settle = log.find((e) => e.type === "CodeSettled" && String((e as { execId?: unknown }).execId) === callId) as
151
- | { result?: unknown; error?: unknown; logs?: ReadonlyArray<string>; tmp?: unknown; size?: unknown; preview?: unknown; note?: unknown }
152
- | undefined
153
- if (settle === undefined) return undefined
154
- // Captured console output rides along: the model reads what its code printed, beside the
155
- // result (the print-to-inspect habit; packages/code/src/sandbox.ts, SandboxResult.logs).
156
- const logs = settle.logs !== undefined && settle.logs.length > 0 ? { logs: settle.logs } : {}
157
- if (settle.error !== undefined) return { error: String(settle.error), ...logs }
158
- // A settle over the spill bound carries a pointer instead of the value: the pointer IS the
159
- // result the model reads, its preview and note naming the ref and the call that loads it
160
- // (turn.test.ts, "a spilled settle").
161
- if (settle.tmp !== undefined) {
162
- return { result: { tmp: settle.tmp, size: settle.size, preview: settle.preview, note: settle.note }, ...logs }
163
- }
164
- return { result: settle.result, ...logs }
165
- }
166
-
167
- const codeServe: Serve = (call, log, answer) => {
168
- if (call.name !== "execute") return undefined
169
- const stamp = call.turn === undefined ? {} : { turn: call.turn }
170
- // Dispatched already: the answer exists once the code reactor settles the execution.
171
- if (log.some((e) => e.type === "CodeDispatched" && String((e as { execId?: unknown }).execId) === call.callId)) {
172
- const outcome = settleFor(log, call.callId)
173
- return outcome === undefined ? [] : [answer(outcome)]
174
- }
175
- const code = String((call.arguments as { code?: unknown } | undefined)?.code ?? "")
176
- return [
177
- transition({
178
- key: `cd:${call.callId}`,
179
- input: { execId: call.callId, code },
180
- act: (input) =>
181
- Effect.gen(function* () {
182
- const at = yield* Clock.currentTimeMillis
183
- return [codeDispatched({ execId: input.execId, code: input.code, ...stamp, at })]
184
- })
185
- })
186
- ]
187
- }
188
-
189
- // codeModeFor is the code-execution capability: one `execute` tool, served by dispatching to
190
- // the code reactor and answered from its settle. Every option has a default, so the bare call
191
- // is the whole capability on defaults. `packages` are the values the code may name; the
192
- // capability's R is the spill store plus what those packages need, and the model's fragment is
193
- // derived from the same values, so what the code can call and what the model is told cannot
194
- // drift. `policy` is the code lane's own (packages/code/src/execute.ts, CodePolicy). `system`
195
- // replaces the derived fragment, as a string or as a projection of the log, for a host that
196
- // names its scope from its own events (capability.test.ts, "codeModeFor takes a system
197
- // fragment"). `packages` infers as a tuple, so a mixed list gives the union of what its members
198
- // require (capability.test.ts, "a mounted package's requirements ride the capability's type").
199
- export const codeModeFor = <
200
- const P extends ReadonlyArray<Package<never>> | ReadonlyArray<Package<unknown>> = readonly []
201
- >(
202
- options: {
203
- readonly policy?: Partial<CodePolicy>
204
- readonly system?: Capability["system"]
205
- readonly packages?: P
206
- } = {}
207
- ): Capability<KeyValueStore.KeyValueStore | PackageRequirements<P[number]>> => {
208
- const packages = (options.packages ?? []) as unknown as P
209
- return {
210
- name: "code",
211
- keys: codeKeys,
212
- reactors: [codeReactorFor(options.policy ?? {}, packages)],
213
- tools: () => [EXECUTE_TOOL],
214
- system: options.system ?? codeSystemFor(packages as ReadonlyArray<Package<unknown>>),
215
- serve: codeServe
216
- }
217
- }
218
-
219
- // codeMode is that capability on defaults, with nothing in scope: the library default work
220
- // surface.
221
- export const codeMode: Capability<KeyValueStore.KeyValueStore> = codeModeFor()
222
-
223
- // A NativeTool is one named tool the model calls directly: its wire shape, and the effect that
224
- // runs it. The effect's failures are the tool's own answer, so a tool that throws returns an
225
- // error the model reads rather than killing the turn.
226
- export interface NativeTool<R = never> {
227
- readonly spec: ToolSpec
228
- readonly run: (input: unknown, context: { readonly callId: string; readonly turn?: string }) => Effect.Effect<unknown, never, R>
229
- }
230
-
231
- // toolList is a fixed list of named tools, the shape every provider's tool calling takes and
232
- // the shape a replicated harness is measured on: each call runs its tool and records the
233
- // return, no lane of its own.
234
- export const toolList = <R = never>(tools: ReadonlyArray<NativeTool<R>>, system: Capability["system"] = ""): Capability<R> => {
235
- const table = new Map(tools.map((t) => [t.spec.name, t]))
236
- return {
237
- name: "tools",
238
- system:
239
- system === ""
240
- ? `You act on the world by calling the tools available to you: ${tools.map((t) => t.spec.name).join(", ")}.`
241
- : system,
242
- tools: () => tools.map((t) => t.spec),
243
- serve: (call, _log, _answer) => {
244
- const tool = table.get(call.name)
245
- if (tool === undefined) return undefined
246
- const stamp = call.turn === undefined ? {} : { turn: call.turn }
247
- return [
248
- transition({
249
- key: `tr:${call.callId}`,
250
- input: { callId: call.callId, arguments: call.arguments, turn: call.turn },
251
- act: (input) =>
252
- Effect.gen(function* () {
253
- const result = yield* tool.run(input.arguments, { callId: input.callId, ...(input.turn === undefined ? {} : { turn: input.turn }) })
254
- const at = yield* Clock.currentTimeMillis
255
- return [toolReturned({ callId: input.callId, result, ...stamp, at })]
256
- })
257
- })
258
- ]
259
- }
260
- }
261
- }
262
-
263
- // The policy capabilities: behavior with nothing to show. Their keys live in the runtime's own
264
- // agent alphabet, so they carry none. Their R names what each reactor needs, all within the
265
- // AgentR the runtime already requires.
266
- export const budgetFor = (policy: Partial<BudgetPolicy>): Capability => ({
267
- name: "budget",
268
- reactors: [budgetReactorFor(policy)]
269
- })
270
- export const budget: Capability = budgetFor({})
271
-
272
- // compactionFor states its policy twice on purpose: to its reactor (the guard) and to the
273
- // render (`context`), so the binding truncates against the same numbers the guard fires on.
274
- export const compactionFor = (policy: Partial<ContextPolicy>): Capability<Infer> => ({
275
- name: "compaction",
276
- reactors: [compactionReactorFor(policy)],
277
- context: policy
278
- })
279
- export const compaction: Capability<Infer> = compactionFor({})
280
-
281
- export const reply: Capability<Router | Self> = { name: "reply", reactors: [replyReactor] }