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/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)`).
|
|
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
|
|
25
|
-
- **Strongly typed, built on Effect.** Typed services and Layers make each
|
|
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
|
|
79
|
+
### Create a component
|
|
80
80
|
|
|
81
|
-
An agent is made of
|
|
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 {
|
|
84
|
+
import type { AgentComponent } from "tardie"
|
|
85
85
|
|
|
86
|
-
const deploys:
|
|
86
|
+
const deploys: AgentComponent = {
|
|
87
87
|
name: "deploys",
|
|
88
|
-
|
|
89
|
-
{
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
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
|
-
`
|
|
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.
|
|
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
|
|
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
|
|
119
|
+
Mount the component beside the built-in parts that this task needs:
|
|
114
120
|
|
|
115
121
|
```ts
|
|
116
|
-
import {
|
|
117
|
-
|
|
118
|
-
const releaseAnalyst =
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
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
|
-
`
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
35
|
-
actor:
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
//
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
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,8 +1,9 @@
|
|
|
1
1
|
import { Clock, Effect } from "effect"
|
|
2
2
|
import { transition, type Reactor } from "tardie/core/actor"
|
|
3
|
-
import { budgetExhausted } from "
|
|
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 "
|
|
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 "
|
|
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 "
|
|
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
|
+
})
|
package/src/agent/index.ts
CHANGED
|
@@ -7,13 +7,13 @@ export {
|
|
|
7
7
|
type ActorDefinition
|
|
8
8
|
} from "./artifact"
|
|
9
9
|
|
|
10
|
-
// The parts a caller lists. An agent is
|
|
11
|
-
//
|
|
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
|
|
67
|
-
//
|
|
68
|
-
export {
|
|
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"
|
package/src/agent/request.ts
CHANGED
|
@@ -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
|
+
})
|