tardie 0.9.0 → 0.10.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.
Files changed (115) hide show
  1. package/README.md +30 -24
  2. package/examples/quickstart/actor.ts +23 -22
  3. package/package.json +24 -7
  4. package/src/agent/actor/artifact.ts +12 -0
  5. package/src/agent/actor/budget.ts +54 -0
  6. package/src/agent/{message-method.ts → actor/message.ts} +18 -9
  7. package/src/agent/actor/permission.ts +51 -0
  8. package/src/agent/components/budget-authority.ts +122 -0
  9. package/src/agent/components/budget.ts +147 -22
  10. package/src/agent/components/code.ts +17 -13
  11. package/src/agent/components/compaction.ts +10 -10
  12. package/src/agent/components/native-output.ts +2 -2
  13. package/src/agent/components/permission-authority.ts +124 -0
  14. package/src/agent/components/permissions.ts +103 -0
  15. package/src/agent/components/repair.ts +6 -6
  16. package/src/agent/components/system.ts +2 -2
  17. package/src/agent/components/tool-list.ts +5 -5
  18. package/src/agent/index.ts +71 -28
  19. package/src/agent/{runtime/infer.ts → inference/reactor.ts} +15 -15
  20. package/src/agent/{request.ts → inference/request.ts} +5 -5
  21. package/src/agent/{usage.ts → inference/usage.ts} +2 -2
  22. package/src/agent/{events.ts → log/events.ts} +102 -48
  23. package/src/agent/{boundary.ts → output/boundary.ts} +4 -4
  24. package/src/agent/{output.ts → output/contract.ts} +4 -4
  25. package/src/agent/{spawn.ts → packages/agents.ts} +75 -205
  26. package/src/agent/runtime/{agent.ts → composition.ts} +17 -14
  27. package/src/agent/{resume.ts → runtime/resume.ts} +4 -4
  28. package/src/agent/runtime/tools.ts +3 -3
  29. package/src/agent/{turn.ts → runtime/turn.ts} +16 -18
  30. package/src/bun/host.ts +15 -18
  31. package/src/bun/registry.ts +1 -1
  32. package/src/bun/workspace.ts +4 -4
  33. package/src/channels/providers/slack.ts +4 -1
  34. package/src/channels/providers/telegram.ts +4 -1
  35. package/src/cli/build.ts +84 -26
  36. package/src/cli/commands.ts +22 -4
  37. package/src/cli/dev.ts +2 -2
  38. package/src/cli/main.ts +2 -2
  39. package/src/cli/render.ts +0 -1
  40. package/src/client/client.ts +2 -2
  41. package/src/client/contract.ts +4 -5
  42. package/src/client/index.ts +1 -1
  43. package/src/client/stream.ts +1 -1
  44. package/src/cloudflare/host.ts +14 -12
  45. package/src/cloudflare/storage.ts +1 -1
  46. package/src/cloudflare/worker.ts +25 -22
  47. package/src/code/{events.ts → execution/events.ts} +3 -3
  48. package/src/code/{ids.ts → execution/ids.ts} +3 -3
  49. package/src/code/{projections.ts → execution/projections.ts} +3 -2
  50. package/src/code/{execute.ts → execution/reactor.ts} +9 -9
  51. package/src/code/{turns.ts → execution/turns.ts} +2 -16
  52. package/src/code/{packages.ts → package/definition.ts} +3 -3
  53. package/src/code/{fetch.ts → package/fetch.ts} +4 -4
  54. package/src/code/{files.ts → package/files.ts} +2 -2
  55. package/src/code/{workspace.ts → package/workspace.ts} +3 -3
  56. package/src/code/{defaults.ts → sandbox/defaults.ts} +1 -1
  57. package/src/code/{store.ts → storage/store.ts} +1 -1
  58. package/src/core/actor/component.ts +26 -0
  59. package/src/core/{component.ts → actor/compose.ts} +7 -38
  60. package/src/core/actor/contract.ts +157 -0
  61. package/src/core/actor/definition.ts +83 -0
  62. package/src/core/actor/index.ts +13 -0
  63. package/src/core/actor/method/call.ts +12 -0
  64. package/src/{agent/method.ts → core/actor/method/definition.ts} +17 -24
  65. package/src/core/actor/method/index.ts +28 -0
  66. package/src/core/actor/method/outgoing.ts +145 -0
  67. package/src/core/actor/method/response.ts +194 -0
  68. package/src/core/actor/method/state.ts +5 -0
  69. package/src/core/actor/reference.ts +18 -0
  70. package/src/core/communication/envelope.ts +22 -2
  71. package/src/core/communication/message.ts +3 -3
  72. package/src/core/log/index.ts +4 -0
  73. package/src/core/log/keys.ts +32 -0
  74. package/src/core/{event-log.ts → log/service.ts} +1 -35
  75. package/src/core/{trace.ts → log/trace.ts} +3 -9
  76. package/src/core/reconciliation/index.ts +3 -0
  77. package/src/core/reconciliation/reactor.ts +7 -0
  78. package/src/core/{actor.ts → reconciliation/reconciler.ts} +9 -56
  79. package/src/core/reconciliation/transition.ts +41 -0
  80. package/src/core/thread/index.ts +1 -0
  81. package/src/core/{thread.ts → thread/lineage.ts} +3 -3
  82. package/src/host/communication/channel.ts +12 -3
  83. package/src/host/deadlock.ts +1 -1
  84. package/src/host/host.ts +12 -17
  85. package/src/model/metadata.ts +1 -1
  86. package/src/model/model.ts +13 -13
  87. package/src/model/output.ts +2 -2
  88. package/src/server/actor.ts +26 -24
  89. package/src/server/api.ts +1 -1
  90. package/src/server/driver-gauge.ts +16 -0
  91. package/src/server/host.ts +20 -21
  92. package/src/server/http.ts +2 -20
  93. package/src/server/main.ts +2 -2
  94. package/src/server/projections.ts +7 -7
  95. package/src/worker-loader/sandbox.ts +2 -2
  96. package/ui/assets/{index-CgJpoZ-O.js → index-OL-NaSyM.js} +2 -2
  97. package/ui/index.html +1 -1
  98. package/src/agent/artifact.ts +0 -29
  99. package/src/agent/components/reply.ts +0 -180
  100. package/src/code/index.ts +0 -13
  101. package/src/core/directory.ts +0 -1
  102. package/src/core/endpoint.ts +0 -1
  103. package/src/core/envelope.ts +0 -1
  104. package/src/core/facets.ts +0 -19
  105. package/src/core/link.ts +0 -3
  106. package/src/core/message.ts +0 -1
  107. package/src/core/router.ts +0 -4
  108. package/src/core/transport.ts +0 -3
  109. /package/src/agent/{model.ts → inference/reference.ts} +0 -0
  110. /package/src/{core → bun}/runtime.ts +0 -0
  111. /package/src/code/{contract.ts → execution/contract.ts} +0 -0
  112. /package/src/code/{errors.ts → execution/errors.ts} +0 -0
  113. /package/src/code/{sandbox.ts → sandbox/service.ts} +0 -0
  114. /package/src/core/{actor-registry.ts → actor/registry.ts} +0 -0
  115. /package/src/core/{event.ts → log/event.ts} +0 -0
package/README.md CHANGED
@@ -13,7 +13,7 @@
13
13
 
14
14
  # Tardigrade
15
15
 
16
- Tardigrade is a typescript framework for building durable, modular agents that can run at the edge. It is inspired by [React](https://react.dev/)'s declarative approach to building user interfaces.
16
+ Tardigrade is a typescript framework for building durable, modular agents that can run on the cloud. It is inspired by [React](https://react.dev/)'s declarative approach to building user interfaces.
17
17
 
18
18
  ### Agents that can self-improve
19
19
  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.
@@ -130,40 +130,46 @@ Mount the component beside the built-in parts that this task needs:
130
130
 
131
131
  ```ts
132
132
  import {
133
- actor, agentMethods, agentsPackage, budget, codeMode,
134
- compaction, defineActor, fetchPackage, filesPackage, infer,
135
- outputValidateOnce, reply, system, workspacePackage
133
+ actor, agentMethods, agentsPackage, budget, budgetAuthority, caller, codeMode,
134
+ compaction, fetchPackage, filesPackage, infer,
135
+ outputValidateOnce, system, workspacePackage
136
136
  } from "tardie"
137
137
 
138
138
  const instructions = system(
139
139
  "You are a release analyst. Identify risky changes and recommend the safest next action."
140
140
  )
141
141
 
142
- const releaseModel = { provider: "openai", default_model: "gpt-5.2" } as const
142
+ const model = { provider: "openai", default_model: "gpt-5.2" } as const
143
143
 
144
- const releaseAnalyst = defineActor({
144
+ const releaseAnalyst = actor({
145
+ // name supplies the actor's stable identity.
145
146
  name: "release-analyst",
147
+ // methods declare how the world can communicate with this actor.
146
148
  methods: agentMethods,
147
- actor: actor(infer([
148
- instructions, // the agent's system prompt
149
- deploys, // recent_deploys and its paired handler
150
- // budget scopes the tool-call limit to the codeMode subtree.
151
- budget([
152
- codeMode([
153
- filesPackage(),
154
- fetchPackage(),
155
- agentsPackage(),
156
- workspacePackage()
157
- ])
158
- ]),
159
- compaction(), // bounded model context
160
- reply, // results for parent agents
161
- outputValidateOnce // validates one structured result without correction
162
- ], releaseModel))
149
+ // components implement those methods and derive transitions from the actor's private log.
150
+ components: [
151
+ // infer handles messages as model loops composed by children.
152
+ infer([
153
+ instructions, // the agent's system prompt
154
+ deploys, // provides recent_deploys tool and its paired handler
155
+ // budget scopes the tool-call limit to the codeMode subtree
156
+ budget([
157
+ codeMode([ // sandboxed code execution
158
+ filesPackage(), // access file system
159
+ fetchPackage(), // make network fetch
160
+ agentsPackage(), // call subagents
161
+ workspacePackage() // sqlite workspace
162
+ ])
163
+ ], { authority: caller() }), // declares authority to grant budget
164
+ compaction(), // bounded model context
165
+ outputValidateOnce // validates structured result once without correction
166
+ ], model),
167
+ budgetAuthority() // budgetAuthority handles requestBudget for this actor.
168
+ ]
163
169
  })
164
170
  ```
165
171
 
166
- `infer` composes the components into an agent loop. Its trailing options select a private provider connection and the default model used through it. `defineActor` gives that loop a stable name and callable interface. `agentMethods` provides a `message` method with `{ text, input?, model? }` input and a string result. `model` is a model ID for that turn; it cannot change the actor's provider connection.
172
+ `infer` composes the components into an agent loop. Its trailing options select a private provider connection and the default model used through it. `actor` binds those components to a stable name and callable interface. `agentMethods` provides `message` and `requestBudget`. Every actor method call is a durable future that remains pending until it receives one completed or failed terminal response. `model` is a model ID for that turn; it cannot change the actor's provider connection.
167
173
 
168
174
  `compaction(policy?)` bounds model context. The host resolves the selected model's window from its catalog snapshot. Compaction fires at 80 percent and keeps a 50 percent tail unless the actor states other ratios:
169
175
 
@@ -178,7 +184,7 @@ When compaction runs, its checkpoint records the applied policy with the summary
178
184
 
179
185
  `codeMode([...components])` combines code packages behind one `execute` tool. Define a package with `definePackage(...)`. Group packages with `composeComponents(...)`.
180
186
 
181
- `budget([...components], policy?)` limits calls to tools derived by its child components. Components beside the wrapper remain outside that budget.
187
+ `budget([...components], { limit?, authority? })` meters calls to tools derived by its child components and closes that subtree at the limit. Components beside the wrapper remain outside that budget. An authority lets an escalatable child call `requestBudget`; `caller()` selects the actor that sent its current message. `budgetAuthority()` is the local automatic handler. Another actor can keep the same method pending while a service or human decides it.
182
188
 
183
189
  This agent can inspect deployments and files, fetch sources, delegate research, and analyze results with JavaScript. Change the package list to create another harness.
184
190
 
@@ -1,7 +1,7 @@
1
1
  import {
2
- actor, agentMethods, agentsPackage, budget, codeMode,
3
- compaction, defineActor, fetchPackage, filesPackage, infer,
4
- outputValidateOnce, reply, system, workspacePackage
2
+ actor, agentMethods, agentsPackage, budget, budgetAuthority, caller, codeMode,
3
+ compaction, fetchPackage, filesPackage, infer,
4
+ outputValidateOnce, system, workspacePackage
5
5
  } from "tardie"
6
6
 
7
7
  // actorName is the stable name used by build, development, and deployment.
@@ -18,26 +18,27 @@ Delegate independent research when it helps.
18
18
  Return a concise answer with concrete findings.
19
19
  `.trim()
20
20
 
21
- export default defineActor({
21
+ export default actor({
22
22
  name: actorName,
23
23
  // methods declares the typed calls this actor accepts.
24
24
  methods: agentMethods,
25
- // actor carries component and output requirements into the host type.
26
- actor: actor(infer([
27
- system(actorInstructions),
28
- // budget scopes the tool-call limit to the codeMode subtree.
29
- budget([
30
- // codeMode gives the model one code tool over the package components listed here.
31
- codeMode([
32
- // codeMode package components grant access to files, HTTP, child agents, and saved results.
33
- filesPackage(), fetchPackage(), agentsPackage(), workspacePackage()
34
- ])
35
- ]),
36
- // reply returns a finished turn to the actor that delegated it.
37
- reply,
38
- // compaction summarizes older context when a long turn outgrows its context window.
39
- compaction(),
40
- // outputValidateOnce validates one structured result when the endpoint supplies no native guarantee.
41
- outputValidateOnce
42
- ], actorModel))
25
+ // components carry implementation and output requirements into the host type.
26
+ components: [
27
+ infer([
28
+ system(actorInstructions),
29
+ // budget scopes the tool-call limit to the codeMode subtree.
30
+ budget([
31
+ // codeMode gives the model one code tool over the package components listed here.
32
+ codeMode([
33
+ // codeMode package components grant access to files, HTTP, child agents, and saved results.
34
+ filesPackage(), fetchPackage(), agentsPackage(), workspacePackage()
35
+ ])
36
+ ], { authority: caller() }),
37
+ // compaction summarizes older context when a long turn outgrows its context window.
38
+ compaction(),
39
+ // outputValidateOnce validates one structured result when the endpoint supplies no native guarantee.
40
+ outputValidateOnce
41
+ ], actorModel),
42
+ budgetAuthority()
43
+ ]
43
44
  })
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tardie",
3
- "version": "0.9.0",
3
+ "version": "0.10.0",
4
4
  "license": "MIT",
5
5
  "author": "Clavia, Inc.",
6
6
  "description": "A durable agent harness. State is a pure function of the log.",
@@ -16,7 +16,7 @@
16
16
  "access": "public"
17
17
  },
18
18
  "tardigrade": {
19
- "sourceTree": "c2d12a57747f767777e828e2a314b1262fa17e25"
19
+ "sourceTree": "9ad5b25bb0b49092e6a3aa93d8c2f35886f769fb"
20
20
  },
21
21
  "files": [
22
22
  "src",
@@ -33,9 +33,27 @@
33
33
  "exports": {
34
34
  ".": "./src/agent/index.ts",
35
35
  "./package.json": "./package.json",
36
- "./core/*": "./src/core/*.ts",
37
- "./code": "./src/code/index.ts",
38
- "./code/*": "./src/code/*.ts",
36
+ "./actor/*": "./src/agent/actor/*.ts",
37
+ "./components/*": "./src/agent/components/*.ts",
38
+ "./inference/*": "./src/agent/inference/*.ts",
39
+ "./log/*": "./src/agent/log/*.ts",
40
+ "./output/*": "./src/agent/output/*.ts",
41
+ "./packages/*": "./src/agent/packages/*.ts",
42
+ "./runtime/*": "./src/agent/runtime/*.ts",
43
+ "./core/actor": "./src/core/actor/index.ts",
44
+ "./core/actor/*": "./src/core/actor/*.ts",
45
+ "./core/communication": "./src/core/communication/index.ts",
46
+ "./core/communication/*": "./src/core/communication/*.ts",
47
+ "./core/log": "./src/core/log/index.ts",
48
+ "./core/log/*": "./src/core/log/*.ts",
49
+ "./core/reconciliation": "./src/core/reconciliation/index.ts",
50
+ "./core/reconciliation/*": "./src/core/reconciliation/*.ts",
51
+ "./core/thread": "./src/core/thread/index.ts",
52
+ "./core/thread/*": "./src/core/thread/*.ts",
53
+ "./code/execution/*": "./src/code/execution/*.ts",
54
+ "./code/package/*": "./src/code/package/*.ts",
55
+ "./code/sandbox/*": "./src/code/sandbox/*.ts",
56
+ "./code/storage/*": "./src/code/storage/*.ts",
39
57
  "./host/*": "./src/host/*.ts",
40
58
  "./channels": "./src/channels/index.ts",
41
59
  "./channels/*": "./src/channels/*.ts",
@@ -48,8 +66,7 @@
48
66
  "./server/*": "./src/server/*.ts",
49
67
  "./cli/*": "./src/cli/*.ts",
50
68
  "./model": "./src/model/model.ts",
51
- "./model/*": "./src/model/*.ts",
52
- "./*": "./src/agent/*.ts"
69
+ "./model/*": "./src/model/*.ts"
53
70
  },
54
71
  "dependencies": {
55
72
  "@aws-sdk/client-bedrock-runtime": "^3.1079.0",
@@ -0,0 +1,12 @@
1
+ export {
2
+ ACTOR_NAME_PATTERN
3
+ } from "tardie/core/actor"
4
+
5
+ export const ACTOR_ARTIFACT_VERSION = 3
6
+
7
+ export interface ActorArtifactManifest {
8
+ readonly schema: typeof ACTOR_ARTIFACT_VERSION
9
+ readonly name: string
10
+ readonly module: string
11
+ readonly digest: string
12
+ }
@@ -0,0 +1,54 @@
1
+ import { Schema } from "effect"
2
+ import { actorMethod } from "tardie/core/actor/method"
3
+ import { budgetRequestReceived } from "../log/events"
4
+
5
+ const PositiveInteger = Schema.Int.pipe(
6
+ Schema.check(Schema.makeFilter((value: number) => value > 0, { title: "positive" }))
7
+ )
8
+
9
+ export const BudgetRequestInput = Schema.Struct({
10
+ request: Schema.String,
11
+ turn: Schema.String,
12
+ reason: Schema.String,
13
+ amount: PositiveInteger
14
+ }).annotate({ identifier: "BudgetRequestInput" })
15
+
16
+ export type BudgetRequestInput = typeof BudgetRequestInput.Type
17
+
18
+ export const BudgetDecision = Schema.Union([
19
+ Schema.Struct({ granted: PositiveInteger }),
20
+ Schema.Struct({ denied: Schema.Literal(true), reason: Schema.optionalKey(Schema.String) })
21
+ ]).annotate({ identifier: "BudgetDecision" })
22
+
23
+ export type BudgetDecision = typeof BudgetDecision.Type
24
+
25
+ // requestBudgetMethod exposes budget negotiation as a unary actor call.
26
+ export const requestBudgetMethod = actorMethod({
27
+ input: BudgetRequestInput,
28
+ output: BudgetDecision,
29
+ event: ({ id, input, at }) => budgetRequestReceived({ id, ...input, at }),
30
+ state: (events, id) => {
31
+ const requested = events.some((event) =>
32
+ event.type === "BudgetRequestReceived" && String((event as { readonly id?: unknown }).id) === id
33
+ )
34
+ if (!requested) return undefined
35
+ const decided = events.find((event) =>
36
+ event.type === "BudgetRequestDecided" && String((event as { readonly callId?: unknown }).callId) === id
37
+ ) as { readonly grant?: unknown; readonly reason?: unknown } | undefined
38
+ const failed = events.find((event) =>
39
+ event.type === "BudgetRequestFailed" && String((event as { readonly callId?: unknown }).callId) === id
40
+ ) as { readonly error?: unknown } | undefined
41
+ if (failed !== undefined) return { status: "failed", error: String(failed.error ?? "budget authority failed") }
42
+ if (decided === undefined) return { status: "pending" }
43
+ const grant = Number(decided.grant ?? 0)
44
+ return grant > 0
45
+ ? { status: "completed", output: { granted: grant } }
46
+ : {
47
+ status: "completed",
48
+ output: {
49
+ denied: true as const,
50
+ ...(typeof decided.reason === "string" && decided.reason !== "" ? { reason: decided.reason } : {})
51
+ }
52
+ }
53
+ }
54
+ })
@@ -1,7 +1,8 @@
1
1
  import { Schema } from "effect"
2
2
  import { messageReceived } from "tardie/core/communication/message"
3
- import { boundaryOf } from "./boundary"
4
- import { actorMethod, actorMethodsOf } from "./method"
3
+ import { boundaryOf } from "../output/boundary"
4
+ import { actorMethod, actorMethodsOf } from "tardie/core/actor/method"
5
+ import { requestBudgetMethod } from "./budget"
5
6
 
6
7
  export const AgentMessageInput = Schema.Struct({
7
8
  text: Schema.String,
@@ -23,16 +24,24 @@ export const agentMessageMethod = actorMethod({
23
24
  at
24
25
  }),
25
26
  state: (events, id) => {
26
- const invoked = events.some((event) =>
27
+ const invoked = events.find((event) =>
27
28
  event.type === "MessageReceived" && (event as { readonly id?: unknown }).id === id
28
- )
29
- if (!invoked) return undefined
29
+ ) as { readonly output?: unknown } | undefined
30
+ if (invoked === undefined) return undefined
31
+ const data = invoked.output === undefined ? undefined : { output: invoked.output }
30
32
  const boundary = boundaryOf(events, id)
31
33
  if (boundary === undefined) return { status: "pending" }
32
- if (boundary.kind === "completed") return { status: "completed", output: boundary.output }
33
- if (boundary.kind === "failed") return { status: "failed", error: boundary.error }
34
- return { status: "blocked", reason: boundary.reason }
34
+ if (boundary.kind === "completed") {
35
+ return { status: "completed", output: boundary.output, ...(data === undefined ? {} : { data }) }
36
+ }
37
+ if (boundary.kind === "failed") {
38
+ return { status: "failed", error: boundary.error, ...(data === undefined ? {} : { data }) }
39
+ }
40
+ return { status: "pending" }
35
41
  }
36
42
  })
37
43
 
38
- export const agentMethods = actorMethodsOf({ message: agentMessageMethod })
44
+ export const agentMethods = actorMethodsOf({
45
+ message: agentMessageMethod,
46
+ requestBudget: requestBudgetMethod
47
+ })
@@ -0,0 +1,51 @@
1
+ import { Schema } from "effect"
2
+ import { actorMethod } from "tardie/core/actor/method"
3
+ import { permissionRequestReceived } from "../log/events"
4
+
5
+ export const PermissionRequestInput = Schema.Struct({
6
+ request: Schema.String,
7
+ turn: Schema.String,
8
+ tool: Schema.String,
9
+ action: Schema.String,
10
+ resource: Schema.optionalKey(Schema.String),
11
+ reason: Schema.String
12
+ }).annotate({ identifier: "PermissionRequestInput" })
13
+
14
+ export type PermissionRequestInput = typeof PermissionRequestInput.Type
15
+
16
+ export const PermissionDecision = Schema.Union([
17
+ Schema.Struct({ granted: Schema.Literal(true) }),
18
+ Schema.Struct({ denied: Schema.Literal(true), reason: Schema.optionalKey(Schema.String) })
19
+ ]).annotate({ identifier: "PermissionDecision" })
20
+
21
+ export type PermissionDecision = typeof PermissionDecision.Type
22
+
23
+ // requestPermissionMethod exposes one-shot tool authorization as a unary actor call.
24
+ export const requestPermissionMethod = actorMethod({
25
+ input: PermissionRequestInput,
26
+ output: PermissionDecision,
27
+ event: ({ id, input, at }) => permissionRequestReceived({ id, ...input, at }),
28
+ state: (events, id) => {
29
+ const received = events.some((event) =>
30
+ event.type === "PermissionRequestReceived" && String((event as { readonly id?: unknown }).id) === id
31
+ )
32
+ if (!received) return undefined
33
+ const decided = events.find((event) =>
34
+ event.type === "PermissionRequestDecided" && String((event as { readonly callId?: unknown }).callId) === id
35
+ ) as { readonly granted?: unknown; readonly reason?: unknown } | undefined
36
+ const failed = events.find((event) =>
37
+ event.type === "PermissionRequestFailed" && String((event as { readonly callId?: unknown }).callId) === id
38
+ ) as { readonly error?: unknown } | undefined
39
+ if (failed !== undefined) return { status: "failed", error: String(failed.error ?? "permission authority failed") }
40
+ if (decided === undefined) return { status: "pending" }
41
+ return decided.granted === true
42
+ ? { status: "completed", output: { granted: true as const } }
43
+ : {
44
+ status: "completed",
45
+ output: {
46
+ denied: true as const,
47
+ ...(typeof decided.reason === "string" && decided.reason !== "" ? { reason: decided.reason } : {})
48
+ }
49
+ }
50
+ }
51
+ })
@@ -0,0 +1,122 @@
1
+ import type { Event } from "tardie/core/log/event"
2
+ import { Schema } from "effect"
3
+ import type { KeyFragment } from "tardie/core/log"
4
+ import { intent, type Transition } from "tardie/core/reconciliation"
5
+ import { handles, type Component } from "tardie/core/actor"
6
+ import { formatActorId, isActorId } from "tardie/core/communication/endpoint"
7
+ import { BudgetDecision } from "../actor/budget"
8
+ import { budgetRequestDecided, budgetRequestFailed } from "../log/events"
9
+ import { requestBudgetMethod } from "../actor/budget"
10
+
11
+ // BudgetRequest describes one durable authority call and supplies its valid decisions.
12
+ export interface BudgetRequest {
13
+ readonly id: string
14
+ readonly reason: string
15
+ readonly amount: number
16
+ readonly turn: string
17
+ readonly from?: string
18
+ readonly grant: (amount?: number) => BudgetDecision
19
+ readonly deny: (reason?: string) => BudgetDecision
20
+ }
21
+
22
+ // DecideBudget is the pure local policy implemented by budgetAuthority.
23
+ export type DecideBudget = (request: BudgetRequest) => BudgetDecision
24
+
25
+ // DEFAULT_BUDGET_DECISION grants the number of tool calls the child requested.
26
+ export const DEFAULT_BUDGET_DECISION: DecideBudget = (request) => request.grant()
27
+
28
+ export interface BudgetAuthorityOptions {
29
+ readonly decide?: DecideBudget
30
+ }
31
+
32
+ // budgetAuthorityKeys owns budget authority calls and their terminal outcomes.
33
+ export const budgetAuthorityKeys: KeyFragment = {
34
+ prefixes: ["bar:", "ba:"],
35
+ keyOf: (event) => {
36
+ const value = event as Record<string, unknown>
37
+ if (event.type === "BudgetRequestReceived") return `bar:${String(value.id)}`
38
+ return event.type === "BudgetRequestDecided" || event.type === "BudgetRequestFailed"
39
+ ? `ba:${String(value.callId)}`
40
+ : undefined
41
+ }
42
+ }
43
+
44
+ const failureMessage = (failure: unknown): string =>
45
+ failure instanceof Error ? failure.message : String(failure)
46
+
47
+ const authorityTransition = (
48
+ log: ReadonlyArray<Event>,
49
+ decide: DecideBudget
50
+ ): Transition<never> | undefined => {
51
+ const received = log.find((event) =>
52
+ event.type === "BudgetRequestReceived" &&
53
+ !log.some((terminal) =>
54
+ (terminal.type === "BudgetRequestDecided" || terminal.type === "BudgetRequestFailed") &&
55
+ String((terminal as { readonly callId?: unknown }).callId) === String((event as { readonly id?: unknown }).id)
56
+ )
57
+ ) as {
58
+ readonly id?: unknown
59
+ readonly turn?: unknown
60
+ readonly reason?: unknown
61
+ readonly amount?: unknown
62
+ readonly link?: { readonly source?: unknown }
63
+ } | undefined
64
+ if (received === undefined) return undefined
65
+
66
+ const id = String(received.id ?? "")
67
+ const amount = Number(received.amount ?? 0)
68
+ const request: BudgetRequest = {
69
+ id,
70
+ turn: String(received.turn ?? ""),
71
+ reason: String(received.reason ?? ""),
72
+ amount,
73
+ ...(isActorId(received.link?.source) ? { from: formatActorId(received.link.source) } : {}),
74
+ grant: (granted = amount) => ({ granted }),
75
+ deny: (reason) => ({ denied: true, ...(reason === undefined ? {} : { reason }) })
76
+ }
77
+
78
+ try {
79
+ const proposed = decide(request)
80
+ if ("granted" in proposed && (!Number.isSafeInteger(proposed.granted) || proposed.granted <= 0)) {
81
+ throw new Error(`budget grant must be a positive integer, got ${JSON.stringify(proposed.granted)}`)
82
+ }
83
+ const decision = Schema.decodeSync(BudgetDecision)(proposed)
84
+ if ("granted" in decision) {
85
+ const grant = decision.granted
86
+ return intent({
87
+ key: `ba:${id}`,
88
+ input: { id, grant },
89
+ events: (input, at) => [budgetRequestDecided({ callId: input.id, grant: input.grant, at })]
90
+ })
91
+ }
92
+ return intent({
93
+ key: `ba:${id}`,
94
+ input: { id, reason: decision.reason },
95
+ events: (input, at) => [budgetRequestDecided({
96
+ callId: input.id,
97
+ grant: 0,
98
+ ...(input.reason === undefined ? {} : { reason: input.reason }),
99
+ at
100
+ })]
101
+ })
102
+ } catch (failure) {
103
+ return intent({
104
+ key: `ba:${id}`,
105
+ input: { id, error: failureMessage(failure) },
106
+ events: (input, at) => [budgetRequestFailed({ callId: input.id, error: input.error, at })]
107
+ })
108
+ }
109
+ }
110
+
111
+ // budgetAuthority handles requestBudget with a pure local decision policy.
112
+ export const budgetAuthority = (options: BudgetAuthorityOptions = {}): Component<undefined> => {
113
+ const decide = options.decide ?? DEFAULT_BUDGET_DECISION
114
+ return handles(requestBudgetMethod, {
115
+ name: "budget-authority",
116
+ keys: budgetAuthorityKeys,
117
+ derive: (log) => {
118
+ const transition = authorityTransition(log, decide)
119
+ return { view: undefined, transitions: transition === undefined ? [] : [transition] }
120
+ }
121
+ })
122
+ }