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.
Files changed (58) hide show
  1. package/README.md +21 -31
  2. package/examples/quickstart/actor.ts +20 -21
  3. package/package.json +3 -1
  4. package/src/agent/boundary.ts +30 -0
  5. package/src/agent/components/budget.ts +6 -5
  6. package/src/agent/components/code.ts +3 -2
  7. package/src/agent/components/compaction.ts +22 -6
  8. package/src/agent/components/repair.ts +86 -0
  9. package/src/agent/components/reply.ts +50 -15
  10. package/src/agent/components/tool-list.ts +3 -2
  11. package/src/agent/events.ts +194 -18
  12. package/src/agent/index.ts +72 -8
  13. package/src/agent/output.ts +568 -0
  14. package/src/agent/request.ts +117 -31
  15. package/src/agent/runtime/agent.ts +129 -34
  16. package/src/agent/runtime/infer.ts +289 -80
  17. package/src/agent/runtime/tools.ts +6 -12
  18. package/src/agent/spawn.ts +168 -50
  19. package/src/agent/turn.ts +11 -6
  20. package/src/bun/communication/index.ts +1 -0
  21. package/src/bun/communication/webhook.ts +65 -0
  22. package/src/bun/host.ts +25 -3
  23. package/src/bun/webhook.ts +1 -0
  24. package/src/channels/index.ts +2 -0
  25. package/src/channels/providers/slack.ts +219 -0
  26. package/src/channels/providers/telegram.ts +161 -0
  27. package/src/channels/slack.ts +1 -0
  28. package/src/channels/telegram.ts +1 -0
  29. package/src/cli/config.ts +15 -3
  30. package/src/cli/setup.ts +54 -9
  31. package/src/core/actor.ts +2 -1
  32. package/src/core/communication/address.ts +55 -0
  33. package/src/core/communication/delivery.ts +25 -0
  34. package/src/core/communication/index.ts +6 -0
  35. package/src/core/communication/link.ts +17 -0
  36. package/src/core/communication/message.ts +56 -0
  37. package/src/core/communication/outbound.ts +15 -0
  38. package/src/core/communication/router.ts +44 -0
  39. package/src/core/component.ts +33 -34
  40. package/src/core/link.ts +3 -0
  41. package/src/core/message.ts +1 -68
  42. package/src/core/router.ts +2 -55
  43. package/src/host/communication/channel.ts +50 -0
  44. package/src/host/communication/index.ts +4 -0
  45. package/src/host/communication/ingress.ts +73 -0
  46. package/src/host/communication/provider.ts +34 -0
  47. package/src/host/communication/webhook.ts +44 -0
  48. package/src/host/host.ts +27 -3
  49. package/src/host/ingress.ts +1 -0
  50. package/src/host/webhook.ts +1 -0
  51. package/src/model/model.ts +277 -91
  52. package/src/model/output.ts +165 -0
  53. package/src/server/actor.ts +12 -12
  54. package/src/server/config.ts +43 -1
  55. package/src/server/host.ts +43 -9
  56. package/ui/assets/{index-q9tUiBh9.js → index-DFdC4Jl-.js} +4 -4
  57. package/ui/index.html +1 -1
  58. 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 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).
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{information},\ \mathrm{transitions}\rbrace = f(\mathrm{log})$$
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
- ### For agents
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 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:
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
- info: {
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 information.
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 information that offered the call and asks it to serve against the current log.
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 { 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
- )
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
- `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.
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
- actorOf,
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
- info: { system: [actorInstructions], tools: [], context: [] },
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
- // 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
- )
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.4.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",
@@ -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: `answer` and
40
- // `request_budget` are the turn's exits, so they never draw the budget down.
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 `outputSchemaOf`, so an
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 no agent information.
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
- info: { system: [], tools: [], context: [] },
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
- info: {
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
- if (!(firedUncovered(log) || (overContext(log, resolved) && atRoundBoundary(log)))) return []
239
- const cut = cutOf(log, resolved)
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(log)
242
- const span = log.slice(keepFromIndex(log, prior.keepFrom), cut.index)
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
- info: { system: [], tools: [], context: [{ component: "compaction", policy }] },
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
- ): { readonly id: string; readonly replyTo?: string; readonly text: string; readonly outcome: "completed" | "failed" } => {
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
- yield* router.deliver(
60
- input.replyTo,
61
- replyEvent({ id: input.id, text: input.text, outcome: input.outcome, from: self, at })
62
- )
63
- return [replyDelivered({ to: input.replyTo, turn: input.id, at })]
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 no agent information.
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
- info: { system: [], tools: [], context: [] },
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
- info: {
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
  })