tardie 0.4.0 → 0.5.0-rc
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 +21 -31
- package/examples/quickstart/actor.ts +20 -21
- package/package.json +3 -1
- package/src/agent/boundary.ts +30 -0
- package/src/agent/components/budget.ts +6 -5
- package/src/agent/components/code.ts +3 -2
- package/src/agent/components/compaction.ts +22 -6
- package/src/agent/components/repair.ts +86 -0
- package/src/agent/components/reply.ts +50 -15
- package/src/agent/components/tool-list.ts +3 -2
- package/src/agent/events.ts +194 -18
- package/src/agent/index.ts +72 -8
- package/src/agent/output.ts +568 -0
- package/src/agent/request.ts +117 -31
- package/src/agent/runtime/agent.ts +129 -34
- package/src/agent/runtime/infer.ts +289 -80
- package/src/agent/runtime/tools.ts +6 -12
- package/src/agent/spawn.ts +168 -50
- package/src/agent/turn.ts +11 -6
- package/src/bun/communication/index.ts +1 -0
- package/src/bun/communication/webhook.ts +65 -0
- package/src/bun/host.ts +25 -3
- package/src/bun/webhook.ts +1 -0
- package/src/channels/index.ts +2 -0
- package/src/channels/providers/slack.ts +219 -0
- package/src/channels/providers/telegram.ts +161 -0
- package/src/channels/slack.ts +1 -0
- package/src/channels/telegram.ts +1 -0
- package/src/cli/config.ts +15 -3
- package/src/cli/setup.ts +54 -9
- package/src/core/actor.ts +2 -1
- package/src/core/communication/address.ts +55 -0
- package/src/core/communication/delivery.ts +25 -0
- package/src/core/communication/index.ts +6 -0
- package/src/core/communication/link.ts +17 -0
- package/src/core/communication/message.ts +56 -0
- package/src/core/communication/outbound.ts +15 -0
- package/src/core/communication/router.ts +44 -0
- package/src/core/component.ts +33 -34
- package/src/core/link.ts +3 -0
- package/src/core/message.ts +1 -68
- package/src/core/router.ts +2 -55
- package/src/host/communication/channel.ts +50 -0
- package/src/host/communication/index.ts +4 -0
- package/src/host/communication/ingress.ts +73 -0
- package/src/host/communication/provider.ts +34 -0
- package/src/host/communication/webhook.ts +44 -0
- package/src/host/host.ts +27 -3
- package/src/host/ingress.ts +1 -0
- package/src/host/webhook.ts +1 -0
- package/src/model/model.ts +277 -91
- package/src/model/output.ts +165 -0
- package/src/server/actor.ts +12 -12
- package/src/server/config.ts +43 -1
- package/src/server/host.ts +43 -9
- package/ui/assets/{index-q9tUiBh9.js → index-DFdC4Jl-.js} +4 -4
- package/ui/index.html +1 -1
- package/src/agent/contract.ts +0 -38
package/README.md
CHANGED
|
@@ -15,9 +15,9 @@ 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
|
|
18
|
+
We took inspiration from React. React derives its component tree and declared effects from state (`{ UI, effects } = f(state)`). Tardigrade derives a view 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{
|
|
20
|
+
$$\lbrace\mathrm{view},\ \mathrm{transitions}\rbrace = f(\mathrm{log})$$
|
|
21
21
|
|
|
22
22
|
## Why Tardigrade
|
|
23
23
|
|
|
@@ -29,17 +29,7 @@ $$\lbrace\mathrm{information},\ \mathrm{transitions}\rbrace = f(\mathrm{log})$$
|
|
|
29
29
|
|
|
30
30
|
## Quickstart
|
|
31
31
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
Copy this prompt into your coding agent, or install the [Tardigrade skill](skills/tardigrade/SKILL.md):
|
|
35
|
-
|
|
36
|
-
```text
|
|
37
|
-
Use https://github.com/clavia-labs/tardigrade and follow skills/tardigrade/SKILL.md to create, author, build, push, and run a local actor. Share its Voyager trace URL.
|
|
38
|
-
```
|
|
39
|
-
|
|
40
|
-
### For developers
|
|
41
|
-
|
|
42
|
-
Install Tardigrade and initialize an editable template actor. Use Bun 1.4 or later.
|
|
32
|
+
Install Tardigrade and initialize an editable template actor. Use Bun 1.4 or later. If you are using a coding agent, the [Tardigrade skill](skills/tardigrade/SKILL.md) can help.
|
|
43
33
|
|
|
44
34
|
```bash
|
|
45
35
|
bun add -g tardie
|
|
@@ -78,7 +68,7 @@ You can use `npm install tardie` instead. Install `tardie@next` to test a releas
|
|
|
78
68
|
|
|
79
69
|
### Create a component
|
|
80
70
|
|
|
81
|
-
An agent is made of components. A component derives
|
|
71
|
+
An agent is made of components. A component derives a view and owed transitions from the log. An agent view includes system fragments, tool bindings, and context policy. This component gives the model one tool and owes no autonomous work:
|
|
82
72
|
|
|
83
73
|
```ts
|
|
84
74
|
import type { AgentComponent } from "tardie"
|
|
@@ -86,7 +76,7 @@ import type { AgentComponent } from "tardie"
|
|
|
86
76
|
const deploys: AgentComponent = {
|
|
87
77
|
name: "deploys",
|
|
88
78
|
derive: () => ({
|
|
89
|
-
|
|
79
|
+
view: {
|
|
90
80
|
system: ["Inspect recent deployments when a release may explain an incident."],
|
|
91
81
|
tools: [{
|
|
92
82
|
spec: {
|
|
@@ -98,7 +88,8 @@ const deploys: AgentComponent = {
|
|
|
98
88
|
answer([{ service: "api", revision: "a17c", summary: "Add rate limiting" }])
|
|
99
89
|
]
|
|
100
90
|
}],
|
|
101
|
-
context: []
|
|
91
|
+
context: [],
|
|
92
|
+
output: []
|
|
102
93
|
},
|
|
103
94
|
transitions: []
|
|
104
95
|
})
|
|
@@ -109,9 +100,9 @@ const deploys: AgentComponent = {
|
|
|
109
100
|
|
|
110
101
|
The call follows one route:
|
|
111
102
|
|
|
112
|
-
1. The component adds `recent_deploys` to its derived
|
|
103
|
+
1. The component adds `recent_deploys` to its derived view.
|
|
113
104
|
2. The model selects it and returns a tool call. Tardigrade records `ToolCalled` in the log.
|
|
114
|
-
3. The shared runtime finds the paired handler in the
|
|
105
|
+
3. The shared runtime finds the paired handler in the view that offered the call and asks it to serve against the current log.
|
|
115
106
|
4. Tardigrade records `ToolReturned`. The next model request includes the result.
|
|
116
107
|
|
|
117
108
|
### Compose an agent
|
|
@@ -119,21 +110,19 @@ The call follows one route:
|
|
|
119
110
|
Mount the component beside the built-in parts that this task needs:
|
|
120
111
|
|
|
121
112
|
```ts
|
|
122
|
-
import {
|
|
123
|
-
|
|
124
|
-
const releaseAnalyst =
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
]
|
|
133
|
-
)
|
|
113
|
+
import { agentOf, budget, codeMode, compaction, outputFailFast, reply } from "tardie"
|
|
114
|
+
|
|
115
|
+
const releaseAnalyst = agentOf([
|
|
116
|
+
deploys, // recent_deploys and its paired handler
|
|
117
|
+
codeMode, // durable JavaScript execution
|
|
118
|
+
budget, // a per-turn code budget
|
|
119
|
+
compaction, // bounded model context
|
|
120
|
+
reply, // results for parent agents
|
|
121
|
+
outputFailFast // structured results without a retry fallback
|
|
122
|
+
])
|
|
134
123
|
```
|
|
135
124
|
|
|
136
|
-
`
|
|
125
|
+
`agentOf` combines the components and carries their service requirements into the host type. The runtime interprets their view 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.
|
|
137
126
|
|
|
138
127
|
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.
|
|
139
128
|
|
|
@@ -202,6 +191,7 @@ Effects have at-least-once execution. Each keyed result is recorded once. Provid
|
|
|
202
191
|
## Learn more
|
|
203
192
|
|
|
204
193
|
- [Quickstart](docs/quickstart.md): build the event loop and its agent components from first principles.
|
|
194
|
+
- [Structured output](docs/output.md): declare a typed result and read its value.
|
|
205
195
|
- [HTTP server](docs/how-to/server.md)
|
|
206
196
|
- [CLI](docs/how-to/cli.md)
|
|
207
197
|
- [Why Tardigrade](docs/explanations/why.md): learn what the log-as-state model makes possible.
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import {
|
|
2
|
-
|
|
3
|
-
agentRuntime,
|
|
2
|
+
agentOf,
|
|
4
3
|
agentsPackage,
|
|
5
4
|
budget,
|
|
6
5
|
codeModeFor,
|
|
@@ -8,6 +7,7 @@ import {
|
|
|
8
7
|
defineActor,
|
|
9
8
|
fetchPackage,
|
|
10
9
|
filesPackage,
|
|
10
|
+
outputFailFast,
|
|
11
11
|
reply,
|
|
12
12
|
workspacePackage
|
|
13
13
|
} from "tardie"
|
|
@@ -28,29 +28,28 @@ Return a concise answer with concrete findings.
|
|
|
28
28
|
const instructions = {
|
|
29
29
|
name: "instructions",
|
|
30
30
|
derive: () => ({
|
|
31
|
-
|
|
31
|
+
view: { system: [actorInstructions], tools: [], context: [], output: [] },
|
|
32
32
|
transitions: []
|
|
33
33
|
})
|
|
34
34
|
}
|
|
35
35
|
|
|
36
36
|
export default defineActor({
|
|
37
37
|
name: actorName,
|
|
38
|
-
//
|
|
39
|
-
actor:
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
//
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
)
|
|
38
|
+
// agentOf carries component and output requirements into the host type.
|
|
39
|
+
actor: agentOf([
|
|
40
|
+
instructions,
|
|
41
|
+
// codeModeFor gives the model one code tool over the packages listed here.
|
|
42
|
+
codeModeFor({
|
|
43
|
+
// packages grant access to local files, HTTP, child agents, and saved tool results.
|
|
44
|
+
packages: [filesPackage(), fetchPackage(), agentsPackage(), workspacePackage()]
|
|
45
|
+
}),
|
|
46
|
+
// reply returns a finished turn to the actor that delegated it.
|
|
47
|
+
reply,
|
|
48
|
+
// budget stops work tools when the turn reaches its tool-call limit.
|
|
49
|
+
budget,
|
|
50
|
+
// compaction summarizes older context when a long turn outgrows its context window.
|
|
51
|
+
compaction,
|
|
52
|
+
// outputFailFast handles structured results on endpoints with no native guarantee without retrying.
|
|
53
|
+
outputFailFast
|
|
54
|
+
])
|
|
56
55
|
})
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "tardie",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0-rc",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"author": "Clavia, Inc.",
|
|
6
6
|
"description": "A durable agent harness. State is a pure function of the log.",
|
|
@@ -34,6 +34,8 @@
|
|
|
34
34
|
"./code": "./src/code/index.ts",
|
|
35
35
|
"./code/*": "./src/code/*.ts",
|
|
36
36
|
"./host/*": "./src/host/*.ts",
|
|
37
|
+
"./channels": "./src/channels/index.ts",
|
|
38
|
+
"./channels/*": "./src/channels/*.ts",
|
|
37
39
|
"./client": "./src/client/index.ts",
|
|
38
40
|
"./client/*": "./src/client/*.ts",
|
|
39
41
|
"./bun/*": "./src/bun/*.ts",
|
package/src/agent/boundary.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { Event } from "tardie/core/event"
|
|
2
2
|
import { turnTerminalOf } from "tardie/code/turns"
|
|
3
|
+
import { canonicalOf, declarationForTurn, type OutputContract } from "./output"
|
|
3
4
|
|
|
4
5
|
// Boundary is where a settle left a turn: a terminal, or a park on a budget ask. The
|
|
5
6
|
// platform's call and resume read it to answer the spawning code. Pure over the log, so a
|
|
@@ -32,3 +33,32 @@ export const boundaryOf = (log: ReadonlyArray<Event>, turn: string): Boundary |
|
|
|
32
33
|
}
|
|
33
34
|
return undefined
|
|
34
35
|
}
|
|
36
|
+
|
|
37
|
+
// outputOf reads a completed turn's result under the contract that turn declared. It returns undefined for a pending or failed turn and throws when the declaration or stored result cannot satisfy the supplied contract (boundary.test.ts, "a turn that declared nothing is never reinterpreted"; runtime/infer.ts, completionOf).
|
|
38
|
+
export const outputOf = <T>(
|
|
39
|
+
contract: OutputContract<T>,
|
|
40
|
+
log: ReadonlyArray<Event>,
|
|
41
|
+
turn: string
|
|
42
|
+
): T | undefined => {
|
|
43
|
+
const terminal = turnTerminalOf(log, turn)
|
|
44
|
+
if (terminal === undefined || terminal.type !== "TurnCompleted") return undefined
|
|
45
|
+
const declared = declarationForTurn(log, turn)
|
|
46
|
+
if (declared.kind !== "contract") {
|
|
47
|
+
throw new Error(
|
|
48
|
+
`turn ${turn} did not declare the contract "${contract.name}", so its result is not a value of that contract` +
|
|
49
|
+
(declared.kind === "invalid" ? `: ${declared.errors.join("; ")}` : "")
|
|
50
|
+
)
|
|
51
|
+
}
|
|
52
|
+
if (canonicalOf(declared.contract) !== canonicalOf(contract)) {
|
|
53
|
+
throw new Error(
|
|
54
|
+
`turn ${turn} declared the contract "${declared.contract.name}", which is not the contract "${contract.name}" this read holds`
|
|
55
|
+
)
|
|
56
|
+
}
|
|
57
|
+
const decoded = declared.contract.decode(JSON.parse(String((terminal as { output?: unknown }).output)))
|
|
58
|
+
if ("errors" in decoded) {
|
|
59
|
+
throw new Error(
|
|
60
|
+
`turn ${turn} completed with a result that misses the contract "${contract.name}":\n${decoded.errors.map((e) => `- ${e}`).join("\n")}`
|
|
61
|
+
)
|
|
62
|
+
}
|
|
63
|
+
return decoded.value as T
|
|
64
|
+
}
|
|
@@ -36,8 +36,9 @@ export const budgetOf = (view: ReadonlyArray<Event>, policy: Partial<BudgetPolic
|
|
|
36
36
|
return base + granted
|
|
37
37
|
}
|
|
38
38
|
|
|
39
|
-
// usedOf counts the tool calls the turn has spent. Only `execute` spends: `
|
|
40
|
-
//
|
|
39
|
+
// usedOf counts the tool calls the turn has spent. Only `execute` spends: `request_budget` is
|
|
40
|
+
// the turn's escalation, so it never draws the budget down, and the final response is no tool
|
|
41
|
+
// call at all (src/output.ts).
|
|
41
42
|
export const usedOf = (view: ReadonlyArray<Event>): number =>
|
|
42
43
|
view.filter((e) => e.type === "ToolCalled" && String((e as { name?: unknown }).name) === "execute").length
|
|
43
44
|
|
|
@@ -63,7 +64,7 @@ export const worldOf = (view: ReadonlyArray<Event>): string | undefined => {
|
|
|
63
64
|
}
|
|
64
65
|
|
|
65
66
|
// BudgetPhase is the budget state of the current turn, read from the most recent lifecycle
|
|
66
|
-
// marker scanning back. It is pure and scoped to the current turn like `
|
|
67
|
+
// marker scanning back. It is pure and scoped to the current turn like `contractOf`, so an
|
|
67
68
|
// earlier turn's wall does not leak. `exhausted`: the wall is up and the turn may still ask.
|
|
68
69
|
// `denied`: the ask was refused, so the turn must answer. `spending`: a grant reopened the
|
|
69
70
|
// budget, or none was ever spent.
|
|
@@ -131,13 +132,13 @@ export const budgetReactorFor = (policy: Partial<BudgetPolicy> = {}): Reactor<ne
|
|
|
131
132
|
// budgetReactor is that reactor on the default ceiling.
|
|
132
133
|
export const budgetReactor: Reactor<never> = budgetReactorFor()
|
|
133
134
|
|
|
134
|
-
// budgetFor derives budget transitions and contributes
|
|
135
|
+
// budgetFor derives budget transitions and contributes an empty agent view.
|
|
135
136
|
export const budgetFor = (policy: Partial<BudgetPolicy>): AgentComponent => {
|
|
136
137
|
const reactor = budgetReactorFor(policy)
|
|
137
138
|
return {
|
|
138
139
|
name: "budget",
|
|
139
140
|
derive: (log) => ({
|
|
140
|
-
|
|
141
|
+
view: { system: [], tools: [], context: [], output: [] },
|
|
141
142
|
transitions: reactor(log)
|
|
142
143
|
})
|
|
143
144
|
}
|
|
@@ -80,10 +80,11 @@ export const codeModeFor = <
|
|
|
80
80
|
name: "code",
|
|
81
81
|
keys: codeKeys,
|
|
82
82
|
derive: (log) => ({
|
|
83
|
-
|
|
83
|
+
view: {
|
|
84
84
|
system: [typeof options.system === "function" ? options.system(log) : options.system ?? codeSystemFor(packages as ReadonlyArray<Package<unknown>>)],
|
|
85
85
|
tools: [{ spec: EXECUTE_TOOL, serve: (call, current, answer) => serveCode(current, call, answer) }],
|
|
86
|
-
context: []
|
|
86
|
+
context: [],
|
|
87
|
+
output: []
|
|
87
88
|
},
|
|
88
89
|
transitions: reactor(log)
|
|
89
90
|
})
|
|
@@ -3,6 +3,7 @@ import { transition, type Reactor } from "tardie/core/actor"
|
|
|
3
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 { projectedOutput } from "../output"
|
|
6
7
|
import { Infer } from "../runtime/infer"
|
|
7
8
|
import type { AgentComponent } from "../runtime/agent"
|
|
8
9
|
|
|
@@ -78,6 +79,13 @@ const renderedChars = (e: Event, policy: ContextPolicy): number => {
|
|
|
78
79
|
return JSON.stringify(v.arguments ?? {}).length
|
|
79
80
|
case "ToolReturned":
|
|
80
81
|
return Math.min(JSON.stringify(v.result ?? null).length, policy.resultRenderCap)
|
|
82
|
+
case "OutputRejected":
|
|
83
|
+
// A rejected response and its reasons render while the correction is owed. A projected one
|
|
84
|
+
// never reaches this function: the measure reads the same projection the render does
|
|
85
|
+
// (src/output.ts, projectedOutput).
|
|
86
|
+
return String(v.text ?? "").length + JSON.stringify(v.errors ?? []).length
|
|
87
|
+
case "OutputRetryRequested":
|
|
88
|
+
return String(v.feedback ?? "").length
|
|
81
89
|
case "TurnCompleted":
|
|
82
90
|
return String(v.output ?? "").length
|
|
83
91
|
case "TurnFailed":
|
|
@@ -92,7 +100,7 @@ const renderedChars = (e: Event, policy: ContextPolicy): number => {
|
|
|
92
100
|
// the estimate is a pure function of the recorded events (compaction.test.ts, "the measure").
|
|
93
101
|
export const estimateTokens = (events: ReadonlyArray<Event>, policy: Partial<ContextPolicy> = {}): number => {
|
|
94
102
|
const resolved = contextPolicyOf(policy)
|
|
95
|
-
return Math.ceil(events.reduce((n, e) => n + renderedChars(e, resolved), 0) / 4)
|
|
103
|
+
return Math.ceil(projectedOutput(events).reduce((n, e) => n + renderedChars(e, resolved), 0) / 4)
|
|
96
104
|
}
|
|
97
105
|
|
|
98
106
|
// checkpointOf returns the last checkpoint: the identity the next span starts from, and the
|
|
@@ -202,6 +210,10 @@ const lineOf = (e: Event, policy: ContextPolicy): string | null => {
|
|
|
202
210
|
return `agent ran: ${clip(JSON.stringify(v.arguments ?? {}), policy.summaryLineCap)}`
|
|
203
211
|
case "ToolReturned":
|
|
204
212
|
return `result: ${clip(JSON.stringify(v.result ?? null), policy.summaryLineCap)}`
|
|
213
|
+
case "OutputRejected":
|
|
214
|
+
return `agent (refused, ${String(v.contract ?? "")}): ${clip(String(v.text ?? ""), policy.summaryLineCap)}`
|
|
215
|
+
case "OutputRetryRequested":
|
|
216
|
+
return `asked again: ${clip(String(v.feedback ?? ""), policy.summaryLineCap)}`
|
|
205
217
|
case "TurnCompleted":
|
|
206
218
|
return `agent: ${String(v.output ?? "")}`
|
|
207
219
|
case "TurnFailed":
|
|
@@ -235,11 +247,15 @@ const firedUncovered = (log: ReadonlyArray<Event>): boolean => {
|
|
|
235
247
|
// model never sees (ContextPolicy above).
|
|
236
248
|
export const compactionReactorFor = (policy: Partial<ContextPolicy> = {}): Reactor<Infer> => (log) => {
|
|
237
249
|
const resolved = contextPolicyOf(policy)
|
|
238
|
-
|
|
239
|
-
|
|
250
|
+
// The projection runs first, so the guard, the cut, and the brief all read the history the
|
|
251
|
+
// model reads. A corrected exchange the render hides can neither trigger a paid pass nor leak
|
|
252
|
+
// its rejected reply into a summary (src/output.ts, projectedOutput).
|
|
253
|
+
const view = projectedOutput(log)
|
|
254
|
+
if (!(firedUncovered(view) || (overContext(view, resolved) && atRoundBoundary(view)))) return []
|
|
255
|
+
const cut = cutOf(view, resolved)
|
|
240
256
|
if (cut === undefined) return []
|
|
241
|
-
const prior = checkpointOf(
|
|
242
|
-
const span =
|
|
257
|
+
const prior = checkpointOf(view)
|
|
258
|
+
const span = view.slice(keepFromIndex(view, prior.keepFrom), cut.index)
|
|
243
259
|
return [
|
|
244
260
|
transition({
|
|
245
261
|
key: `cc:${cut.keepFrom}`,
|
|
@@ -282,7 +298,7 @@ export const compactionFor = (policy: Partial<ContextPolicy>): AgentComponent<In
|
|
|
282
298
|
return {
|
|
283
299
|
name: "compaction",
|
|
284
300
|
derive: (log) => ({
|
|
285
|
-
|
|
301
|
+
view: { system: [], tools: [], context: [{ component: "compaction", policy }], output: [] },
|
|
286
302
|
transitions: reactor(log)
|
|
287
303
|
})
|
|
288
304
|
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import type { Event } from "tardie/core/event"
|
|
2
|
+
import { turnView } from "tardie/code/turns"
|
|
3
|
+
import { correctionAttemptsErrors, declaredOutputOf, type OutputFallback } from "../output"
|
|
4
|
+
import { defineOutputFallback, type OutputFallbackComponent } from "../runtime/agent"
|
|
5
|
+
|
|
6
|
+
// RepairPolicy sets the correction limit and completed-history projection. `attempts` counts correction requests after the initial request (src/output.ts, projectedOutput).
|
|
7
|
+
export interface RepairPolicy {
|
|
8
|
+
readonly attempts: number
|
|
9
|
+
readonly projectHistory: boolean
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export const DEFAULT_REPAIR_POLICY: RepairPolicy = { attempts: 2, projectHistory: true }
|
|
13
|
+
|
|
14
|
+
// repairPolicyOf applies DEFAULT_REPAIR_POLICY and validates each override (turn.test.ts, "a bound that is not a whole count of asks is refused where it is stated").
|
|
15
|
+
export const repairPolicyOf = (policy: Partial<RepairPolicy> = {}): RepairPolicy => {
|
|
16
|
+
const attempts = policy.attempts ?? DEFAULT_REPAIR_POLICY.attempts
|
|
17
|
+
const problems = correctionAttemptsErrors(attempts)
|
|
18
|
+
if (problems.length > 0) throw new Error(`the repair policy is not applicable: ${problems.join("; ")}`)
|
|
19
|
+
const projectHistory = policy.projectHistory ?? DEFAULT_REPAIR_POLICY.projectHistory
|
|
20
|
+
if (typeof projectHistory !== "boolean") {
|
|
21
|
+
throw new Error(
|
|
22
|
+
`the repair policy is not applicable: projectHistory must be true or false, got ${JSON.stringify(projectHistory)}`
|
|
23
|
+
)
|
|
24
|
+
}
|
|
25
|
+
return { attempts, projectHistory }
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// repairFallback returns the validated fallback record interpreted by the infer reactor.
|
|
29
|
+
export const repairFallback = (policy: Partial<RepairPolicy> = {}): OutputFallback => {
|
|
30
|
+
const resolved = repairPolicyOf(policy)
|
|
31
|
+
return {
|
|
32
|
+
kind: "repair",
|
|
33
|
+
name: "repair",
|
|
34
|
+
attempts: resolved.attempts,
|
|
35
|
+
projectHistory: resolved.projectHistory
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// outputSystemFor returns the schema instruction used only in fallback mode (runtime/agent.ts, OutputFragment).
|
|
40
|
+
export const outputSystemFor = (name: string, schema: unknown): string =>
|
|
41
|
+
`Your final reply for this turn must be JSON conforming to the schema "${name}":\n${JSON.stringify(schema)}\nReply with that JSON alone: no prose around it, no code fence.`
|
|
42
|
+
|
|
43
|
+
// declaredSystem returns the fallback instruction for the current declared contract.
|
|
44
|
+
const declaredSystem = (log: ReadonlyArray<Event>): { readonly system?: string } => {
|
|
45
|
+
const declared = declaredOutputOf(turnView(log))
|
|
46
|
+
return declared.kind === "contract"
|
|
47
|
+
? { system: outputSystemFor(declared.contract.name, declared.contract.schema) }
|
|
48
|
+
: {}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// outputRepairFor derives the framework correction loop under a stated policy.
|
|
52
|
+
export const outputRepairFor = (policy: Partial<RepairPolicy> = {}): OutputFallbackComponent => {
|
|
53
|
+
const fallback = repairFallback(policy)
|
|
54
|
+
return defineOutputFallback({
|
|
55
|
+
name: "output.repair",
|
|
56
|
+
derive: (log: ReadonlyArray<Event>) => ({
|
|
57
|
+
view: {
|
|
58
|
+
system: [],
|
|
59
|
+
tools: [],
|
|
60
|
+
context: [],
|
|
61
|
+
output: [{ component: "output.repair", fallback, ...declaredSystem(log) }]
|
|
62
|
+
},
|
|
63
|
+
transitions: []
|
|
64
|
+
})
|
|
65
|
+
})
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// outputRepair is the component under the default policy.
|
|
69
|
+
export const outputRepair: OutputFallbackComponent = outputRepairFor()
|
|
70
|
+
|
|
71
|
+
// FAIL_FAST_FALLBACK validates one result and schedules no correction.
|
|
72
|
+
export const FAIL_FAST_FALLBACK: OutputFallback = { kind: "local", name: "fail-fast" }
|
|
73
|
+
|
|
74
|
+
// outputFailFast contributes the fail-fast fallback and its contract instruction (turn.test.ts, "the fail-fast implementation").
|
|
75
|
+
export const outputFailFast: OutputFallbackComponent = defineOutputFallback({
|
|
76
|
+
name: "output.fail-fast",
|
|
77
|
+
derive: (log: ReadonlyArray<Event>) => ({
|
|
78
|
+
view: {
|
|
79
|
+
system: [],
|
|
80
|
+
tools: [],
|
|
81
|
+
context: [],
|
|
82
|
+
output: [{ component: "output.fail-fast", fallback: FAIL_FAST_FALLBACK, ...declaredSystem(log) }]
|
|
83
|
+
},
|
|
84
|
+
transitions: []
|
|
85
|
+
})
|
|
86
|
+
})
|
|
@@ -1,11 +1,20 @@
|
|
|
1
1
|
import { Clock, Effect } from "effect"
|
|
2
|
-
import { Router } from "tardie/core/router"
|
|
2
|
+
import { Router } from "tardie/core/communication/router"
|
|
3
3
|
import { Self, transition, type Reactor } from "tardie/core/actor"
|
|
4
4
|
import { replyDelivered } from "../events"
|
|
5
5
|
import type { Event } from "tardie/core/event"
|
|
6
|
-
import { replyEvent } from "tardie/core/message"
|
|
6
|
+
import { replyEvent } from "tardie/core/communication/message"
|
|
7
7
|
import { turnTerminalOf, replyView } from "tardie/code/turns"
|
|
8
8
|
import type { AgentComponent } from "../runtime/agent"
|
|
9
|
+
import { linkOf, reverseLink, type Link } from "tardie/core/communication/link"
|
|
10
|
+
import {
|
|
11
|
+
formatActorAddress,
|
|
12
|
+
isActorAddress,
|
|
13
|
+
isProviderAddress,
|
|
14
|
+
parseActorAddress,
|
|
15
|
+
type ActorAddress,
|
|
16
|
+
type ProviderAddress
|
|
17
|
+
} from "tardie/core/communication/address"
|
|
9
18
|
|
|
10
19
|
// The reply reactor: report the turn's terminal home. When the inbound named a `replyTo`, the
|
|
11
20
|
// terminal goes back to that actor as a plain `MessageReceived`, and the caller folds it as a
|
|
@@ -19,9 +28,15 @@ import type { AgentComponent } from "../runtime/agent"
|
|
|
19
28
|
// owedTurn returns the turn being reported: the view's head, and its stamped terminal.
|
|
20
29
|
const owedTurn = (
|
|
21
30
|
log: ReadonlyArray<Event>
|
|
22
|
-
): {
|
|
31
|
+
): {
|
|
32
|
+
readonly id: string
|
|
33
|
+
readonly link?: Link<unknown, ActorAddress>
|
|
34
|
+
readonly replyTo?: string
|
|
35
|
+
readonly text: string
|
|
36
|
+
readonly outcome: "completed" | "failed"
|
|
37
|
+
} => {
|
|
23
38
|
const view = replyView(log)
|
|
24
|
-
const inbound = view[0] as { id?: unknown; replyTo?: unknown } | undefined
|
|
39
|
+
const inbound = view[0] as { id?: unknown; link?: unknown; replyTo?: unknown } | undefined
|
|
25
40
|
const id = String(inbound?.id)
|
|
26
41
|
const terminal = turnTerminalOf(log, id) as
|
|
27
42
|
| { output?: unknown; error?: unknown }
|
|
@@ -31,6 +46,9 @@ const owedTurn = (
|
|
|
31
46
|
}
|
|
32
47
|
return {
|
|
33
48
|
id,
|
|
49
|
+
...(typeof inbound.link === "object" && inbound.link !== null && "source" in inbound.link && "target" in inbound.link
|
|
50
|
+
? { link: inbound.link as Link<unknown, ActorAddress> }
|
|
51
|
+
: {}),
|
|
34
52
|
...(inbound.replyTo === undefined ? {} : { replyTo: String(inbound.replyTo) }),
|
|
35
53
|
text: terminal.error === undefined ? String(terminal.output) : `error: ${String(terminal.error)}`,
|
|
36
54
|
// The outcome rides as a typed field, so a reader never sniffs the text for failure.
|
|
@@ -51,26 +69,43 @@ export const replyReactor: Reactor<Router | Self> = (log) => {
|
|
|
51
69
|
act: (input) =>
|
|
52
70
|
Effect.gen(function* () {
|
|
53
71
|
const at = yield* Clock.currentTimeMillis
|
|
54
|
-
if (input.replyTo === undefined) {
|
|
55
|
-
return [replyDelivered({ turn: input.id, at })]
|
|
56
|
-
}
|
|
57
|
-
const router = yield* Router
|
|
58
72
|
const self = yield* Self
|
|
59
|
-
|
|
60
|
-
input.
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
73
|
+
const event = replyEvent({
|
|
74
|
+
id: input.id,
|
|
75
|
+
text: input.text,
|
|
76
|
+
outcome: input.outcome,
|
|
77
|
+
from: formatActorAddress(self),
|
|
78
|
+
at
|
|
79
|
+
})
|
|
80
|
+
if (input.link !== undefined && isProviderAddress(input.link.source)) {
|
|
81
|
+
const router = yield* Router
|
|
82
|
+
yield* router.deliver(
|
|
83
|
+
reverseLink(input.link as Link<ProviderAddress, ActorAddress>),
|
|
84
|
+
event
|
|
85
|
+
)
|
|
86
|
+
return [replyDelivered({ to: input.link.source.provider, turn: input.id, at })]
|
|
87
|
+
}
|
|
88
|
+
if (input.link !== undefined && isActorAddress(input.link.source)) {
|
|
89
|
+
const router = yield* Router
|
|
90
|
+
yield* router.deliver(reverseLink(input.link as Link<ActorAddress, ActorAddress>), event)
|
|
91
|
+
return [replyDelivered({ to: formatActorAddress(input.link.source), turn: input.id, at })]
|
|
92
|
+
}
|
|
93
|
+
if (input.replyTo !== undefined) {
|
|
94
|
+
const router = yield* Router
|
|
95
|
+
yield* router.deliver(linkOf(self, parseActorAddress(input.replyTo)), event)
|
|
96
|
+
return [replyDelivered({ to: input.replyTo, turn: input.id, at })]
|
|
97
|
+
}
|
|
98
|
+
return [replyDelivered({ turn: input.id, at })]
|
|
64
99
|
})
|
|
65
100
|
})
|
|
66
101
|
]
|
|
67
102
|
}
|
|
68
103
|
|
|
69
|
-
// reply derives parent-delivery transitions and contributes
|
|
104
|
+
// reply derives parent-delivery transitions and contributes an empty agent view.
|
|
70
105
|
export const reply: AgentComponent<Router | Self> = {
|
|
71
106
|
name: "reply",
|
|
72
107
|
derive: (log) => ({
|
|
73
|
-
|
|
108
|
+
view: { system: [], tools: [], context: [], output: [] },
|
|
74
109
|
transitions: replyReactor(log)
|
|
75
110
|
})
|
|
76
111
|
}
|
|
@@ -18,7 +18,7 @@ export const toolList = <R = never>(
|
|
|
18
18
|
): AgentComponent<R> => ({
|
|
19
19
|
name: "tools",
|
|
20
20
|
derive: (log) => ({
|
|
21
|
-
|
|
21
|
+
view: {
|
|
22
22
|
system: [
|
|
23
23
|
(typeof system === "function" ? system(log) : system) ||
|
|
24
24
|
`You act on the world by calling the tools available to you: ${tools.map((tool) => tool.spec.name).join(", ")}.`
|
|
@@ -41,7 +41,8 @@ export const toolList = <R = never>(
|
|
|
41
41
|
]
|
|
42
42
|
}
|
|
43
43
|
})) as ReadonlyArray<AgentTool<unknown>>,
|
|
44
|
-
context: []
|
|
44
|
+
context: [],
|
|
45
|
+
output: []
|
|
45
46
|
},
|
|
46
47
|
transitions: []
|
|
47
48
|
})
|