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.
- package/README.md +44 -35
- package/examples/quickstart/actor.ts +24 -17
- package/package.json +1 -1
- package/src/agent/{budget.ts → components/budget.ts} +16 -1
- package/src/agent/components/code.ts +93 -0
- package/src/agent/{compaction.ts → components/compaction.ts} +17 -2
- package/src/agent/{reply.ts → components/reply.ts} +11 -1
- package/src/agent/components/tool-list.ts +48 -0
- package/src/agent/index.ts +33 -10
- package/src/agent/request.ts +2 -2
- package/src/agent/runtime/agent.ts +136 -0
- package/src/agent/{infer.ts → runtime/infer.ts} +7 -7
- package/src/agent/{tools.ts → runtime/tools.ts} +12 -12
- package/src/agent/spawn.ts +4 -4
- package/src/agent/turn.ts +8 -8
- package/src/client/contract.ts +1 -1
- package/src/code/execute.ts +3 -3
- package/src/code/index.ts +2 -2
- package/src/code/packages.ts +5 -5
- package/src/core/component.ts +102 -0
- package/src/model/model.ts +2 -2
- package/src/server/actor.ts +12 -8
- package/src/agent/capability.ts +0 -281
package/src/agent/capability.ts
DELETED
|
@@ -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] }
|