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.
- package/README.md +30 -24
- package/examples/quickstart/actor.ts +23 -22
- package/package.json +24 -7
- package/src/agent/actor/artifact.ts +12 -0
- package/src/agent/actor/budget.ts +54 -0
- package/src/agent/{message-method.ts → actor/message.ts} +18 -9
- package/src/agent/actor/permission.ts +51 -0
- package/src/agent/components/budget-authority.ts +122 -0
- package/src/agent/components/budget.ts +147 -22
- package/src/agent/components/code.ts +17 -13
- package/src/agent/components/compaction.ts +10 -10
- package/src/agent/components/native-output.ts +2 -2
- package/src/agent/components/permission-authority.ts +124 -0
- package/src/agent/components/permissions.ts +103 -0
- package/src/agent/components/repair.ts +6 -6
- package/src/agent/components/system.ts +2 -2
- package/src/agent/components/tool-list.ts +5 -5
- package/src/agent/index.ts +71 -28
- package/src/agent/{runtime/infer.ts → inference/reactor.ts} +15 -15
- package/src/agent/{request.ts → inference/request.ts} +5 -5
- package/src/agent/{usage.ts → inference/usage.ts} +2 -2
- package/src/agent/{events.ts → log/events.ts} +102 -48
- package/src/agent/{boundary.ts → output/boundary.ts} +4 -4
- package/src/agent/{output.ts → output/contract.ts} +4 -4
- package/src/agent/{spawn.ts → packages/agents.ts} +75 -205
- package/src/agent/runtime/{agent.ts → composition.ts} +17 -14
- package/src/agent/{resume.ts → runtime/resume.ts} +4 -4
- package/src/agent/runtime/tools.ts +3 -3
- package/src/agent/{turn.ts → runtime/turn.ts} +16 -18
- package/src/bun/host.ts +15 -18
- package/src/bun/registry.ts +1 -1
- package/src/bun/workspace.ts +4 -4
- package/src/channels/providers/slack.ts +4 -1
- package/src/channels/providers/telegram.ts +4 -1
- package/src/cli/build.ts +84 -26
- package/src/cli/commands.ts +22 -4
- package/src/cli/dev.ts +2 -2
- package/src/cli/main.ts +2 -2
- package/src/cli/render.ts +0 -1
- package/src/client/client.ts +2 -2
- package/src/client/contract.ts +4 -5
- package/src/client/index.ts +1 -1
- package/src/client/stream.ts +1 -1
- package/src/cloudflare/host.ts +14 -12
- package/src/cloudflare/storage.ts +1 -1
- package/src/cloudflare/worker.ts +25 -22
- package/src/code/{events.ts → execution/events.ts} +3 -3
- package/src/code/{ids.ts → execution/ids.ts} +3 -3
- package/src/code/{projections.ts → execution/projections.ts} +3 -2
- package/src/code/{execute.ts → execution/reactor.ts} +9 -9
- package/src/code/{turns.ts → execution/turns.ts} +2 -16
- package/src/code/{packages.ts → package/definition.ts} +3 -3
- package/src/code/{fetch.ts → package/fetch.ts} +4 -4
- package/src/code/{files.ts → package/files.ts} +2 -2
- package/src/code/{workspace.ts → package/workspace.ts} +3 -3
- package/src/code/{defaults.ts → sandbox/defaults.ts} +1 -1
- package/src/code/{store.ts → storage/store.ts} +1 -1
- package/src/core/actor/component.ts +26 -0
- package/src/core/{component.ts → actor/compose.ts} +7 -38
- package/src/core/actor/contract.ts +157 -0
- package/src/core/actor/definition.ts +83 -0
- package/src/core/actor/index.ts +13 -0
- package/src/core/actor/method/call.ts +12 -0
- package/src/{agent/method.ts → core/actor/method/definition.ts} +17 -24
- package/src/core/actor/method/index.ts +28 -0
- package/src/core/actor/method/outgoing.ts +145 -0
- package/src/core/actor/method/response.ts +194 -0
- package/src/core/actor/method/state.ts +5 -0
- package/src/core/actor/reference.ts +18 -0
- package/src/core/communication/envelope.ts +22 -2
- package/src/core/communication/message.ts +3 -3
- package/src/core/log/index.ts +4 -0
- package/src/core/log/keys.ts +32 -0
- package/src/core/{event-log.ts → log/service.ts} +1 -35
- package/src/core/{trace.ts → log/trace.ts} +3 -9
- package/src/core/reconciliation/index.ts +3 -0
- package/src/core/reconciliation/reactor.ts +7 -0
- package/src/core/{actor.ts → reconciliation/reconciler.ts} +9 -56
- package/src/core/reconciliation/transition.ts +41 -0
- package/src/core/thread/index.ts +1 -0
- package/src/core/{thread.ts → thread/lineage.ts} +3 -3
- package/src/host/communication/channel.ts +12 -3
- package/src/host/deadlock.ts +1 -1
- package/src/host/host.ts +12 -17
- package/src/model/metadata.ts +1 -1
- package/src/model/model.ts +13 -13
- package/src/model/output.ts +2 -2
- package/src/server/actor.ts +26 -24
- package/src/server/api.ts +1 -1
- package/src/server/driver-gauge.ts +16 -0
- package/src/server/host.ts +20 -21
- package/src/server/http.ts +2 -20
- package/src/server/main.ts +2 -2
- package/src/server/projections.ts +7 -7
- package/src/worker-loader/sandbox.ts +2 -2
- package/ui/assets/{index-CgJpoZ-O.js → index-OL-NaSyM.js} +2 -2
- package/ui/index.html +1 -1
- package/src/agent/artifact.ts +0 -29
- package/src/agent/components/reply.ts +0 -180
- package/src/code/index.ts +0 -13
- package/src/core/directory.ts +0 -1
- package/src/core/endpoint.ts +0 -1
- package/src/core/envelope.ts +0 -1
- package/src/core/facets.ts +0 -19
- package/src/core/link.ts +0 -3
- package/src/core/message.ts +0 -1
- package/src/core/router.ts +0 -4
- package/src/core/transport.ts +0 -3
- /package/src/agent/{model.ts → inference/reference.ts} +0 -0
- /package/src/{core → bun}/runtime.ts +0 -0
- /package/src/code/{contract.ts → execution/contract.ts} +0 -0
- /package/src/code/{errors.ts → execution/errors.ts} +0 -0
- /package/src/code/{sandbox.ts → sandbox/service.ts} +0 -0
- /package/src/core/{actor-registry.ts → actor/registry.ts} +0 -0
- /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
|
|
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,
|
|
135
|
-
outputValidateOnce,
|
|
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
|
|
142
|
+
const model = { provider: "openai", default_model: "gpt-5.2" } as const
|
|
143
143
|
|
|
144
|
-
const releaseAnalyst =
|
|
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
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
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. `
|
|
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],
|
|
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,
|
|
4
|
-
outputValidateOnce,
|
|
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
|
|
21
|
+
export default actor({
|
|
22
22
|
name: actorName,
|
|
23
23
|
// methods declares the typed calls this actor accepts.
|
|
24
24
|
methods: agentMethods,
|
|
25
|
-
//
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
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.
|
|
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": "
|
|
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
|
-
"./
|
|
37
|
-
"./
|
|
38
|
-
"./
|
|
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 "
|
|
4
|
-
import { actorMethod, actorMethodsOf } from "
|
|
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.
|
|
27
|
+
const invoked = events.find((event) =>
|
|
27
28
|
event.type === "MessageReceived" && (event as { readonly id?: unknown }).id === id
|
|
28
|
-
)
|
|
29
|
-
if (
|
|
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")
|
|
33
|
-
|
|
34
|
-
|
|
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({
|
|
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
|
+
}
|