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 CHANGED
@@ -15,14 +15,14 @@ A durable and modular agent harness built for self-improvement.
15
15
  ### A harness made for self-improvement
16
16
  As models get increasingly smart, they will be capable of writing their own harnesses to improve themselves ([Meta-Harness](https://arxiv.org/abs/2603.28052)). A harness that is too rigid and complex is a bottleneck to this. We need something more composable, and easy to author.
17
17
 
18
- We took inspiration from React. React derives the component tree as a function of state (`UI = f(state)`). Similarly, Tardigrade defines the harness as a set of state transitions derived from the event log, an idea with roots in [Harel's statecharts](https://www.sciencedirect.com/science/article/pii/0167642387900359).
18
+ We took inspiration from React. React derives the component tree as a function of state (`UI = f(state)`). Tardigrade derives information and state transitions from the event log, an idea with roots in [Harel's statecharts](https://www.sciencedirect.com/science/article/pii/0167642387900359).
19
19
 
20
- $$\lbrace\mathrm{transitions}\rbrace = f(\mathrm{log})$$
20
+ $$\lbrace\mathrm{information},\ \mathrm{transitions}\rbrace = f(\mathrm{log})$$
21
21
 
22
22
  ## Why Tardigrade
23
23
 
24
- - **Composable harness.** Add tools, code execution, budgets, compaction, and replies as independent capabilities.
25
- - **Strongly typed, built on Effect.** Typed services and Layers make each capability's dependencies explicit. A missing service fails during compile.
24
+ - **Composable harness.** Add tools, code execution, budgets, compaction, and replies as independent components.
25
+ - **Strongly typed, built on Effect.** Typed services and Layers make each component's dependencies explicit. A missing service fails during compile.
26
26
  - **Crash proof.** A durable host derives unfinished work from the stored log.
27
27
  - **Serverless.** All you need is a durable store, no process has to stay alive. Any new invocation reads the log, runs the transitions it owes, and settles.
28
28
  - **Inspect and improve every run.** Log as core supports native debugging, replay, and experiments with state forked from any checkpoint.
@@ -76,55 +76,64 @@ bun add tardie
76
76
 
77
77
  You can use `npm install tardie` instead. Install `tardie@next` to test a release candidate.
78
78
 
79
- ### Create a capability
79
+ ### Create a component
80
80
 
81
- An agent is made of capabilities. A capability is one value with two halves: what the model is shown (`tools`, `system`), and how the calls that come back are handled (`serve`). This one gives the model a single tool:
81
+ An agent is made of components. A component derives information and owed transitions from the log. Agent information includes system fragments, tool bindings, and context policy. This component gives the model one tool and owes no autonomous work:
82
82
 
83
83
  ```ts
84
- import type { Capability } from "tardie"
84
+ import type { AgentComponent } from "tardie"
85
85
 
86
- const deploys: Capability = {
86
+ const deploys: AgentComponent = {
87
87
  name: "deploys",
88
- tools: () => [
89
- {
90
- name: "recent_deploys",
91
- description: "List recent production deploys",
92
- inputSchema: { type: "object", properties: {}, additionalProperties: false }
93
- }
94
- ],
95
- serve: (call, log, answer) => {
96
- if (call.name !== "recent_deploys") return undefined
97
- return [answer([{ service: "api", revision: "a17c", summary: "Add rate limiting" }])]
98
- }
88
+ derive: () => ({
89
+ info: {
90
+ system: ["Inspect recent deployments when a release may explain an incident."],
91
+ tools: [{
92
+ spec: {
93
+ name: "recent_deploys",
94
+ description: "List recent production deploys",
95
+ inputSchema: { type: "object", properties: {}, additionalProperties: false }
96
+ },
97
+ serve: (_call, _log, answer) => [
98
+ answer([{ service: "api", revision: "a17c", summary: "Add rate limiting" }])
99
+ ]
100
+ }],
101
+ context: []
102
+ },
103
+ transitions: []
104
+ })
99
105
  }
100
106
  ```
101
107
 
102
- `tools` derives what the model is offered from the log; a constant capability ignores it. `serve` handles a call that comes back: `answer` mints the transition that records the result, and returning `undefined` passes the call to the next capability. Replace the sample result with a call to your deployment API.
108
+ `derive` is a pure log projection. Each tool binding keeps its specification and handler together, so a tool derived for the model is routable by construction. `answer` mints the transition that records the result. Replace the sample result with a call to your deployment API.
103
109
 
104
110
  The call follows one route:
105
111
 
106
- 1. `tools` adds `recent_deploys` to the next model request.
112
+ 1. The component adds `recent_deploys` to its derived information.
107
113
  2. The model selects it and returns a tool call. Tardigrade records `ToolCalled` in the log.
108
- 3. The shared router asks each mounted capability's `serve` to handle the call. `deploys` matches the name and answers.
114
+ 3. The shared runtime finds the paired handler in the information that offered the call and asks it to serve against the current log.
109
115
  4. Tardigrade records `ToolReturned`. The next model request includes the result.
110
116
 
111
117
  ### Compose an agent
112
118
 
113
- Mount the capability beside the built-in parts that this task needs:
119
+ Mount the component beside the built-in parts that this task needs:
114
120
 
115
121
  ```ts
116
- import { agentOf, budget, codeMode, compaction, reply } from "tardie"
117
-
118
- const releaseAnalyst = agentOf([
119
- deploys, // recent_deploys and its handler
120
- codeMode, // durable JavaScript execution
121
- budget, // a per-turn code budget
122
- compaction, // bounded model context
123
- reply // results for parent agents
124
- ])
122
+ import { actorOf, agentRuntime, budget, codeMode, compaction, reply } from "tardie"
123
+
124
+ const releaseAnalyst = actorOf(
125
+ agentRuntime(),
126
+ [
127
+ deploys, // recent_deploys and its paired handler
128
+ codeMode, // durable JavaScript execution
129
+ budget, // a per-turn code budget
130
+ compaction, // bounded model context
131
+ reply // results for parent agents
132
+ ]
133
+ )
125
134
  ```
126
135
 
127
- `agentOf` combines every model-facing part and runtime handler. The model sees `recent_deploys` and `execute` in one request. Policy capabilities react to the same log.
136
+ `actorOf` combines the components under `agentRuntime`. The runtime interprets their information as inference and tool routing, while the core actor reconciles their transitions. The model sees `recent_deploys` and `execute` in one request. Policy components derive work from the same log.
128
137
 
129
138
  This agent can inspect deployments, analyze results with JavaScript, compact a long investigation, and report to a parent agent. Change the list to create another harness.
130
139
 
@@ -134,7 +143,7 @@ A run can follow this path:
134
143
  MessageReceived -> recent_deploys -> execute -> TurnCompleted
135
144
  ```
136
145
 
137
- Each action and result becomes an event that every capability can interpret.
146
+ Each action and result becomes an event that every component can interpret.
138
147
 
139
148
  ### Run the composition
140
149
 
@@ -192,7 +201,7 @@ Effects have at-least-once execution. Each keyed result is recorded once. Provid
192
201
 
193
202
  ## Learn more
194
203
 
195
- - [Quickstart](docs/quickstart.md): build the event loop and its agent capabilities from first principles.
204
+ - [Quickstart](docs/quickstart.md): build the event loop and its agent components from first principles.
196
205
  - [HTTP server](docs/how-to/server.md)
197
206
  - [CLI](docs/how-to/cli.md)
198
207
  - [Why Tardigrade](docs/explanations/why.md): learn what the log-as-state model makes possible.
@@ -1,5 +1,6 @@
1
1
  import {
2
- agentOf,
2
+ actorOf,
3
+ agentRuntime,
3
4
  agentsPackage,
4
5
  budget,
5
6
  codeModeFor,
@@ -26,24 +27,30 @@ Return a concise answer with concrete findings.
26
27
 
27
28
  const instructions = {
28
29
  name: "instructions",
29
- system: actorInstructions
30
+ derive: () => ({
31
+ info: { system: [actorInstructions], tools: [], context: [] },
32
+ transitions: []
33
+ })
30
34
  }
31
35
 
32
36
  export default defineActor({
33
37
  name: actorName,
34
- // agentOf combines small capabilities into the actor's behavior.
35
- actor: agentOf([
36
- instructions,
37
- // codeModeFor gives the model one code tool over the packages listed here.
38
- codeModeFor({
39
- // packages grant access to local files, HTTP, child agents, and saved tool results.
40
- packages: [filesPackage(), fetchPackage(), agentsPackage(), workspacePackage()]
41
- }),
42
- // reply returns a finished turn to the actor that delegated it.
43
- reply,
44
- // budget stops work tools when the turn reaches its tool-call limit.
45
- budget,
46
- // compaction summarizes older context when a long turn outgrows its context window.
47
- compaction
48
- ])
38
+ // actorOf composes components under the explicit agent information runtime.
39
+ actor: actorOf(
40
+ agentRuntime(),
41
+ [
42
+ instructions,
43
+ // codeModeFor gives the model one code tool over the packages listed here.
44
+ codeModeFor({
45
+ // packages grant access to local files, HTTP, child agents, and saved tool results.
46
+ packages: [filesPackage(), fetchPackage(), agentsPackage(), workspacePackage()]
47
+ }),
48
+ // reply returns a finished turn to the actor that delegated it.
49
+ reply,
50
+ // budget stops work tools when the turn reaches its tool-call limit.
51
+ budget,
52
+ // compaction summarizes older context when a long turn outgrows its context window.
53
+ compaction
54
+ ]
55
+ )
49
56
  })
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tardie",
3
- "version": "0.3.0",
3
+ "version": "0.4.0",
4
4
  "license": "MIT",
5
5
  "author": "Clavia, Inc.",
6
6
  "description": "A durable agent harness. State is a pure function of the log.",
@@ -1,8 +1,9 @@
1
1
  import { Clock, Effect } from "effect"
2
2
  import { transition, type Reactor } from "tardie/core/actor"
3
- import { budgetExhausted } from "./events"
3
+ import { budgetExhausted } from "../events"
4
4
  import type { Event } from "tardie/core/event"
5
5
  import { turnHead, turnView } from "tardie/code/turns"
6
+ import type { AgentComponent } from "../runtime/agent"
6
7
 
7
8
  // The budget reactor observes the turn's tool spend and fires BudgetExhausted once when it
8
9
  // passes the brief's budget. Detection lives here; enforcement lives with the tools reactor,
@@ -129,3 +130,17 @@ export const budgetReactorFor = (policy: Partial<BudgetPolicy> = {}): Reactor<ne
129
130
 
130
131
  // budgetReactor is that reactor on the default ceiling.
131
132
  export const budgetReactor: Reactor<never> = budgetReactorFor()
133
+
134
+ // budgetFor derives budget transitions and contributes no agent information.
135
+ export const budgetFor = (policy: Partial<BudgetPolicy>): AgentComponent => {
136
+ const reactor = budgetReactorFor(policy)
137
+ return {
138
+ name: "budget",
139
+ derive: (log) => ({
140
+ info: { system: [], tools: [], context: [] },
141
+ transitions: reactor(log)
142
+ })
143
+ }
144
+ }
145
+
146
+ export const budget: AgentComponent = budgetFor({})
@@ -0,0 +1,93 @@
1
+ import { Clock, Effect } from "effect"
2
+ import type { KeyValueStore } from "effect/unstable/persistence"
3
+ import { transition, type Transition } from "tardie/core/actor"
4
+ import type { Event } from "tardie/core/event"
5
+ import { codeDispatched, codeKeys } from "tardie/code/events"
6
+ import { codeReactorFor, type CodePolicy } from "tardie/code/execute"
7
+ import type { Package, PackageRequirements } from "tardie/code/packages"
8
+ import type { AgentComponent } from "../runtime/agent"
9
+ import type { Answer, PendingCall } from "../runtime/tools"
10
+ import type { ToolSpec } from "../request"
11
+
12
+ const EXECUTE_TOOL: ToolSpec = {
13
+ name: "execute",
14
+ description:
15
+ "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).",
16
+ inputSchema: {
17
+ type: "object",
18
+ properties: { code: { type: "string", description: "The JavaScript body to run." } },
19
+ required: ["code"],
20
+ additionalProperties: false
21
+ }
22
+ }
23
+
24
+ const CODE_SYSTEM_LEAD = "You act on the world by calling the execute tool with JavaScript; the packages in scope are:"
25
+ export const CODE_SYSTEM = `${CODE_SYSTEM_LEAD}\nnone`
26
+
27
+ // codeSystemFor names each package on its own line as name and description.
28
+ export const codeSystemFor = (packages: ReadonlyArray<Package<unknown>>): string =>
29
+ `${CODE_SYSTEM_LEAD}\n${packages.length === 0 ? "none" : packages.map((p) => `${p.name}: ${p.description}`).join("\n")}`
30
+
31
+ const settleFor = (
32
+ log: ReadonlyArray<Event>,
33
+ callId: string
34
+ ): { result?: unknown; error?: string; logs?: ReadonlyArray<string> } | undefined => {
35
+ const settle = log.find((e) => e.type === "CodeSettled" && String((e as { execId?: unknown }).execId) === callId) as
36
+ | { result?: unknown; error?: unknown; logs?: ReadonlyArray<string>; tmp?: unknown; size?: unknown; preview?: unknown; note?: unknown }
37
+ | undefined
38
+ if (settle === undefined) return undefined
39
+ const logs = settle.logs !== undefined && settle.logs.length > 0 ? { logs: settle.logs } : {}
40
+ if (settle.error !== undefined) return { error: String(settle.error), ...logs }
41
+ if (settle.tmp !== undefined) {
42
+ return { result: { tmp: settle.tmp, size: settle.size, preview: settle.preview, note: settle.note }, ...logs }
43
+ }
44
+ return { result: settle.result, ...logs }
45
+ }
46
+
47
+ const serveCode = (log: ReadonlyArray<Event>, call: PendingCall, answer: Answer): ReadonlyArray<Transition<never>> => {
48
+ const stamp = call.turn === undefined ? {} : { turn: call.turn }
49
+ if (log.some((e) => e.type === "CodeDispatched" && String((e as { execId?: unknown }).execId) === call.callId)) {
50
+ const outcome = settleFor(log, call.callId)
51
+ return outcome === undefined ? [] : [answer(outcome)]
52
+ }
53
+ const code = String((call.arguments as { code?: unknown } | undefined)?.code ?? "")
54
+ return [
55
+ transition({
56
+ key: `cd:${call.callId}`,
57
+ input: { execId: call.callId, code },
58
+ act: (input) =>
59
+ Effect.gen(function* () {
60
+ const at = yield* Clock.currentTimeMillis
61
+ return [codeDispatched({ execId: input.execId, code: input.code, ...stamp, at })]
62
+ })
63
+ })
64
+ ]
65
+ }
66
+
67
+ // codeModeFor derives the execute tool and code transitions from the same log and package scope.
68
+ export const codeModeFor = <
69
+ const P extends ReadonlyArray<Package<never>> | ReadonlyArray<Package<unknown>> = readonly []
70
+ >(
71
+ options: {
72
+ readonly policy?: Partial<CodePolicy>
73
+ readonly system?: string | ((log: ReadonlyArray<Event>) => string)
74
+ readonly packages?: P
75
+ } = {}
76
+ ): AgentComponent<KeyValueStore.KeyValueStore | PackageRequirements<P[number]>> => {
77
+ const packages = (options.packages ?? []) as unknown as P
78
+ const reactor = codeReactorFor(options.policy ?? {}, packages)
79
+ return {
80
+ name: "code",
81
+ keys: codeKeys,
82
+ derive: (log) => ({
83
+ info: {
84
+ system: [typeof options.system === "function" ? options.system(log) : options.system ?? codeSystemFor(packages as ReadonlyArray<Package<unknown>>)],
85
+ tools: [{ spec: EXECUTE_TOOL, serve: (call, current, answer) => serveCode(current, call, answer) }],
86
+ context: []
87
+ },
88
+ transitions: reactor(log)
89
+ })
90
+ }
91
+ }
92
+
93
+ export const codeMode: AgentComponent<KeyValueStore.KeyValueStore> = codeModeFor()
@@ -1,9 +1,10 @@
1
1
  import { Clock, Effect } from "effect"
2
2
  import { transition, type Reactor } from "tardie/core/actor"
3
- import { compactionCompleted } from "./events"
3
+ import { compactionCompleted } from "../events"
4
4
  import type { Event } from "tardie/core/event"
5
5
  import { turnOf, turnView } from "tardie/code/turns"
6
- import { Infer } from "./infer"
6
+ import { Infer } from "../runtime/infer"
7
+ import type { AgentComponent } from "../runtime/agent"
7
8
 
8
9
  // The compaction reactor: a pure observer of the context size, with the hysteresis design. A
9
10
  // guard fires compaction at a resolved tool round, any moment the open turn awaits no call, when
@@ -274,3 +275,17 @@ export const compactionReactorFor = (policy: Partial<ContextPolicy> = {}): React
274
275
  // compactionReactor is that reactor on the default policy. An agent on another policy builds its
275
276
  // own with `compactionReactorFor` and hands the same policy to its render.
276
277
  export const compactionReactor: Reactor<Infer> = compactionReactorFor()
278
+
279
+ // compactionFor derives one context contribution and the transitions governed by that policy.
280
+ export const compactionFor = (policy: Partial<ContextPolicy>): AgentComponent<Infer> => {
281
+ const reactor = compactionReactorFor(policy)
282
+ return {
283
+ name: "compaction",
284
+ derive: (log) => ({
285
+ info: { system: [], tools: [], context: [{ component: "compaction", policy }] },
286
+ transitions: reactor(log)
287
+ })
288
+ }
289
+ }
290
+
291
+ export const compaction: AgentComponent<Infer> = compactionFor({})
@@ -1,10 +1,11 @@
1
1
  import { Clock, Effect } from "effect"
2
2
  import { Router } from "tardie/core/router"
3
3
  import { Self, transition, type Reactor } from "tardie/core/actor"
4
- import { replyDelivered } from "./events"
4
+ import { replyDelivered } from "../events"
5
5
  import type { Event } from "tardie/core/event"
6
6
  import { replyEvent } from "tardie/core/message"
7
7
  import { turnTerminalOf, replyView } from "tardie/code/turns"
8
+ import type { AgentComponent } from "../runtime/agent"
8
9
 
9
10
  // The reply reactor: report the turn's terminal home. When the inbound named a `replyTo`, the
10
11
  // terminal goes back to that actor as a plain `MessageReceived`, and the caller folds it as a
@@ -64,3 +65,12 @@ export const replyReactor: Reactor<Router | Self> = (log) => {
64
65
  })
65
66
  ]
66
67
  }
68
+
69
+ // reply derives parent-delivery transitions and contributes no agent information.
70
+ export const reply: AgentComponent<Router | Self> = {
71
+ name: "reply",
72
+ derive: (log) => ({
73
+ info: { system: [], tools: [], context: [] },
74
+ transitions: replyReactor(log)
75
+ })
76
+ }
@@ -0,0 +1,48 @@
1
+ import { Clock, Effect } from "effect"
2
+ import { transition } from "tardie/core/actor"
3
+ import type { Event } from "tardie/core/event"
4
+ import { toolReturned } from "../events"
5
+ import type { ToolSpec } from "../request"
6
+ import type { AgentComponent, AgentTool } from "../runtime/agent"
7
+
8
+ // NativeTool is one named tool whose effect returns its model-visible result.
9
+ export interface NativeTool<R = never> {
10
+ readonly spec: ToolSpec
11
+ readonly run: (input: unknown, context: { readonly callId: string; readonly turn?: string }) => Effect.Effect<unknown, never, R>
12
+ }
13
+
14
+ // toolList derives fixed tool bindings that pair every specification with its effect handler.
15
+ export const toolList = <R = never>(
16
+ tools: ReadonlyArray<NativeTool<R>>,
17
+ system: string | ((log: ReadonlyArray<Event>) => string) = ""
18
+ ): AgentComponent<R> => ({
19
+ name: "tools",
20
+ derive: (log) => ({
21
+ info: {
22
+ system: [
23
+ (typeof system === "function" ? system(log) : system) ||
24
+ `You act on the world by calling the tools available to you: ${tools.map((tool) => tool.spec.name).join(", ")}.`
25
+ ],
26
+ tools: tools.map((tool): AgentTool<R> => ({
27
+ spec: tool.spec,
28
+ serve: (call) => {
29
+ const stamp = call.turn === undefined ? {} : { turn: call.turn }
30
+ return [
31
+ transition({
32
+ key: `tr:${call.callId}`,
33
+ input: { callId: call.callId, arguments: call.arguments, turn: call.turn },
34
+ act: (input) =>
35
+ Effect.gen(function* () {
36
+ const result = yield* tool.run(input.arguments, { callId: input.callId, ...(input.turn === undefined ? {} : { turn: input.turn }) })
37
+ const at = yield* Clock.currentTimeMillis
38
+ return [toolReturned({ callId: input.callId, result, ...stamp, at })]
39
+ })
40
+ })
41
+ ]
42
+ }
43
+ })) as ReadonlyArray<AgentTool<unknown>>,
44
+ context: []
45
+ },
46
+ transitions: []
47
+ })
48
+ })
@@ -7,13 +7,13 @@ export {
7
7
  type ActorDefinition
8
8
  } from "./artifact"
9
9
 
10
- // The parts a caller lists. An agent is capabilities over one log; the reactors underneath
11
- // remain reachable for a bespoke assembly.
12
- export { inferReactorFor, Infer, DEFAULT_INFER_POLICY, type InferPolicy, type InferRequest, type Render } from "./infer"
13
- export { budgetReactorFor, DEFAULT_BUDGET_POLICY, type BudgetPolicy } from "./budget"
14
- export { toolsReactorFrom, type Answer, type PendingCall, type Serve } from "./tools"
15
- export { replyReactor } from "./reply"
16
- export { compactionReactorFor, DEFAULT_CONTEXT_POLICY, type ContextPolicy } from "./compaction"
10
+ // The parts a caller lists. An agent is components over one log; the reactors underneath remain
11
+ // reachable for a bespoke assembly.
12
+ export { inferReactorFor, Infer, DEFAULT_INFER_POLICY, type InferPolicy, type InferRequest, type Render } from "./runtime/infer"
13
+ export { budgetReactorFor, DEFAULT_BUDGET_POLICY, type BudgetPolicy } from "./components/budget"
14
+ export { toolsReactorFrom, type Answer, type PendingCall, type Serve } from "./runtime/tools"
15
+ export { replyReactor } from "./components/reply"
16
+ export { compactionReactorFor, DEFAULT_CONTEXT_POLICY, type ContextPolicy } from "./components/compaction"
17
17
  export { agentKeys } from "./events"
18
18
  export { resumeTurn, type ResumeTurnOptions, type TurnDriver } from "./resume"
19
19
  export {
@@ -63,6 +63,29 @@ export {
63
63
  type FetchPolicy
64
64
  } from "tardie/code/fetch"
65
65
 
66
- // The capability assembly: code mode is the default, and an agent measured against a fixed
67
- // tool list mounts its own (capability.ts).
68
- export { agentOf, renderOf, codeMode, codeModeFor, CODE_SYSTEM, codeSystemFor, toolList, reply, budget, budgetFor, compaction, compactionFor, type Capability, type NativeTool } from "./capability"
66
+ // The component assembly: code mode is the default, and an agent measured against a fixed tool
67
+ // list mounts its own (runtime/agent.ts).
68
+ export {
69
+ AGENT_INFO_ALGEBRA,
70
+ agentRuntime,
71
+ renderOf,
72
+ type AgentComponent,
73
+ type AgentInfo,
74
+ type AgentTool,
75
+ type ContextFragment
76
+ } from "./runtime/agent"
77
+ export { codeMode, codeModeFor, CODE_SYSTEM, codeSystemFor } from "./components/code"
78
+ export { toolList, type NativeTool } from "./components/tool-list"
79
+ export { budget, budgetFor } from "./components/budget"
80
+ export { compaction, compactionFor } from "./components/compaction"
81
+ export { reply } from "./components/reply"
82
+ export {
83
+ actorOf,
84
+ composeComponents,
85
+ reactorOf,
86
+ type Component,
87
+ type ComponentRequirements,
88
+ type ComponentRuntime,
89
+ type Derivation,
90
+ type InfoAlgebra
91
+ } from "tardie/core/component"
@@ -1,7 +1,7 @@
1
1
  import type { Event } from "tardie/core/event"
2
- import { checkpointOf, contextPolicyOf, keepFromIndex, type ContextPolicy } from "./compaction"
2
+ import { checkpointOf, contextPolicyOf, keepFromIndex, type ContextPolicy } from "./components/compaction"
3
3
  import { outputSchemaOf } from "./contract"
4
- import { budgetSpent, canRequestBudget } from "./budget"
4
+ import { budgetSpent, canRequestBudget } from "./components/budget"
5
5
 
6
6
  // The model request, decided from the trajectory: system prompt, tool surface, message
7
7
  // projection. Domain policy lives with the agent; the platform maps these provider-agnostic
@@ -0,0 +1,136 @@
1
+ import type { Reactor, Transition } from "tardie/core/actor"
2
+ import { composeComponents, type Component, type ComponentRuntime, type InfoAlgebra } from "tardie/core/component"
3
+ import { messageKeys } from "tardie/core/message"
4
+ import type { Event } from "tardie/core/event"
5
+ import type { ToolSpec } from "../request"
6
+ import { agentKeys } from "../events"
7
+ import { inferReactorFor, type InferPolicy } from "./infer"
8
+ import { toolsReactorFrom, type Answer, type PendingCall } from "./tools"
9
+ import type { ContextPolicy } from "../components/compaction"
10
+ import type { AgentR } from "../turn"
11
+
12
+ // AgentTool pairs one model-visible tool specification with the handler for calls to that tool.
13
+ // A derived tool is therefore advertised and routable from the same value.
14
+ export interface AgentTool<R = never> {
15
+ readonly spec: ToolSpec
16
+ readonly serve: (
17
+ call: PendingCall,
18
+ log: ReadonlyArray<Event>,
19
+ answer: Answer
20
+ ) => ReadonlyArray<Transition<never, R>>
21
+ }
22
+
23
+ // ContextFragment names one component's context policy contribution. contextOf rejects
24
+ // conflicting fields, so composition cannot hide a policy override.
25
+ export interface ContextFragment {
26
+ readonly component: string
27
+ readonly policy: Partial<ContextPolicy>
28
+ }
29
+
30
+ // AgentInfo is the information an agent runtime interprets. Arrays retain component order and
31
+ // postpone collision policy until the complete derivation is available.
32
+ export interface AgentInfo {
33
+ readonly system: ReadonlyArray<string>
34
+ readonly tools: ReadonlyArray<AgentTool<unknown>>
35
+ readonly context: ReadonlyArray<ContextFragment>
36
+ }
37
+
38
+ // AgentComponent is a core component whose information is interpreted by the agent runtime.
39
+ export type AgentComponent<R = never> = Component<AgentInfo, R>
40
+
41
+ // AGENT_INFO_ALGEBRA preserves every information contribution in component order. renderOf
42
+ // applies the agent-specific collision and rendering rules to the combined value.
43
+ export const AGENT_INFO_ALGEBRA: InfoAlgebra<AgentInfo> = {
44
+ empty: { system: [], tools: [], context: [] },
45
+ combine: (left, right) => ({
46
+ system: [...left.system, ...right.system],
47
+ tools: [...left.tools, ...right.tools],
48
+ context: [...left.context, ...right.context]
49
+ })
50
+ }
51
+
52
+ const contextOf = (fragments: ReadonlyArray<ContextFragment>): Partial<ContextPolicy> => {
53
+ const context: Partial<Record<keyof ContextPolicy, number>> = {}
54
+ const owners = new Map<keyof ContextPolicy, string>()
55
+ for (const fragment of fragments) {
56
+ for (const [field, value] of Object.entries(fragment.policy) as Array<[keyof ContextPolicy, number]>) {
57
+ const prior = context[field]
58
+ if (prior !== undefined && prior !== value) {
59
+ throw new Error(`context field "${field}" declared by components ${owners.get(field)} and ${fragment.component}`)
60
+ }
61
+ context[field] = value
62
+ owners.set(field, fragment.component)
63
+ }
64
+ }
65
+ return context
66
+ }
67
+
68
+ const checkedTools = (tools: ReadonlyArray<AgentTool<unknown>>): ReadonlyArray<AgentTool<unknown>> => {
69
+ const names = new Set<string>()
70
+ for (const tool of tools) {
71
+ if (names.has(tool.spec.name)) throw new Error(`tool "${tool.spec.name}" declared more than once`)
72
+ names.add(tool.spec.name)
73
+ }
74
+ return tools
75
+ }
76
+
77
+ const infoFrom = <R>(components: ReadonlyArray<AgentComponent<R>>, log: ReadonlyArray<Event>): AgentInfo =>
78
+ composeComponents("agent.info", AGENT_INFO_ALGEBRA, components).derive(log).info
79
+
80
+ // offerLogFor returns the prefix from which inference offered a pending call's tools. ModelCalled
81
+ // is appended before inference, so the preceding prefix is exactly the log passed to render
82
+ // (infer.ts, inferReactorFor; tla/Component.tla, OfferedIsRoutable). Calls created outside
83
+ // inference have no mark and use the current log.
84
+ const offerLogFor = (log: ReadonlyArray<Event>, call: PendingCall): ReadonlyArray<Event> => {
85
+ const called = log.findIndex(
86
+ (event) => event.type === "ToolCalled" && String((event as { callId?: unknown }).callId) === call.callId
87
+ )
88
+ if (called === -1) return log
89
+ for (let index = called - 1; index >= 0; index--) {
90
+ const event = log[index]!
91
+ if (event.type !== "ModelCalled") continue
92
+ const turn = (event as { turn?: unknown }).turn
93
+ if (call.turn === undefined || turn === undefined || String(turn) === call.turn) return log.slice(0, index)
94
+ }
95
+ return log
96
+ }
97
+
98
+ const renderInfo = (
99
+ info: AgentInfo
100
+ ): { readonly system: string; readonly tools: ReadonlyArray<ToolSpec>; readonly context: Partial<ContextPolicy> } => ({
101
+ system: info.system.filter((fragment) => fragment !== "").join("\n"),
102
+ tools: checkedTools(info.tools).map((tool) => tool.spec),
103
+ context: contextOf(info.context)
104
+ })
105
+
106
+ // renderOf derives the model request from the same component information that routing reads.
107
+ export const renderOf = <R>(
108
+ components: ReadonlyArray<AgentComponent<R>>,
109
+ log: ReadonlyArray<Event>
110
+ ): { readonly system: string; readonly tools: ReadonlyArray<ToolSpec>; readonly context: Partial<ContextPolicy> } =>
111
+ renderInfo(infoFrom(components, log))
112
+
113
+ // agentRuntime interprets AgentInfo as inference and tool-routing reactors. actorOf supplies the
114
+ // composed information projection and adds each component's own transition projection.
115
+ export const agentRuntime = (
116
+ policy: Partial<InferPolicy> = {}
117
+ ): ComponentRuntime<AgentInfo, AgentR> => ({
118
+ name: "agent",
119
+ algebra: AGENT_INFO_ALGEBRA,
120
+ keys: [messageKeys, agentKeys],
121
+ reactors: <C>(infoOf: (log: ReadonlyArray<Event>) => AgentInfo): ReadonlyArray<Reactor<AgentR | C>> => {
122
+ const toolsOf = (log: ReadonlyArray<Event>): ReadonlyArray<AgentTool<unknown>> => checkedTools(infoOf(log).tools)
123
+ const offeredTools = (log: ReadonlyArray<Event>, call: PendingCall): ReadonlyArray<AgentTool<unknown>> =>
124
+ toolsOf(offerLogFor(log, call))
125
+ const serve = (call: PendingCall, log: ReadonlyArray<Event>, answer: Answer) => {
126
+ const tool = offeredTools(log, call).find((candidate) => candidate.spec.name === call.name)
127
+ return tool?.serve(call, log, answer) as ReadonlyArray<Transition<never, AgentR | C>> | undefined
128
+ }
129
+
130
+ renderInfo(infoOf([]))
131
+ return [
132
+ inferReactorFor(policy, (log) => renderInfo(infoOf(log))) as Reactor<AgentR | C>,
133
+ toolsReactorFrom(serve, (log, call) => offeredTools(log, call).map((tool) => tool.spec))
134
+ ]
135
+ }
136
+ })