tardie 0.26.0-rc.0 → 0.26.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 (75) hide show
  1. package/package.json +9 -8
  2. package/src/agent/component/compaction/model.ts +32 -0
  3. package/src/agent/component/compaction.ts +10 -19
  4. package/src/agent/inference/access.ts +1 -184
  5. package/src/agent/inference/contract.ts +2 -6
  6. package/src/agent/inference/error.ts +1 -36
  7. package/src/agent/inference/machine.ts +2 -2
  8. package/src/agent/inference/model/continuation.ts +14 -0
  9. package/src/agent/{binding → inference/model}/index.ts +11 -20
  10. package/src/agent/{binding → inference/model}/output.ts +4 -9
  11. package/src/agent/{binding → inference/model}/prompt.ts +3 -5
  12. package/src/agent/{binding → inference/model}/response.ts +4 -4
  13. package/src/agent/inference/model/settings.ts +11 -0
  14. package/src/agent/inference/observer.ts +1 -39
  15. package/src/agent/inference/reference.ts +1 -18
  16. package/src/agent/inference/request.ts +2 -2
  17. package/src/agent/inference/retry.ts +2 -17
  18. package/src/agent/inference/usage.ts +2 -8
  19. package/src/agent/output/contract.ts +2 -2
  20. package/src/agent/projection/cost.ts +56 -0
  21. package/src/agent/runtime/composition.ts +1 -1
  22. package/src/agent/testing/durable-inference.ts +33 -0
  23. package/src/agent/testing/inference.ts +3 -3
  24. package/src/cli/commands.ts +1 -0
  25. package/src/cli/init.ts +8 -9
  26. package/src/client/contract.ts +3 -136
  27. package/src/cloudflare/assembly.ts +3 -4
  28. package/src/cloudflare/worker.ts +3 -3
  29. package/src/model/access.ts +184 -0
  30. package/src/model/catalog/availability.ts +1 -1
  31. package/src/model/catalog/index.ts +1 -1
  32. package/src/model/catalog/metadata.ts +1 -1
  33. package/src/model/catalog/page.ts +2 -2
  34. package/src/model/catalog/repository.ts +2 -2
  35. package/src/model/catalog/schema.ts +138 -0
  36. package/src/model/config.ts +9 -8
  37. package/src/model/error.ts +36 -0
  38. package/src/model/host.ts +21 -18
  39. package/src/model/index.ts +2 -2
  40. package/src/model/output.ts +7 -0
  41. package/src/model/pricing.ts +10 -0
  42. package/src/model/providers/anthropic.ts +2 -1
  43. package/src/model/providers/bedrock-transport.ts +1 -1
  44. package/src/model/providers/bedrock.ts +2 -1
  45. package/src/model/providers/directory.ts +4 -0
  46. package/src/model/providers/layer.ts +9 -5
  47. package/src/model/providers/openai-compat.ts +2 -1
  48. package/src/model/providers/openai.ts +2 -1
  49. package/src/model/providers/openrouter.ts +12 -0
  50. package/src/model/providers/options.ts +24 -21
  51. package/src/model/providers/usage.ts +5 -0
  52. package/src/model/reference.ts +26 -0
  53. package/src/model/selection.ts +5 -4
  54. package/src/model/{binding/index.ts → services.ts} +12 -9
  55. package/src/{agent/binding → model}/settings.ts +6 -14
  56. package/src/{agent/binding/observer.ts → model/stream/delivery.ts} +22 -1
  57. package/src/model/stream/observer.ts +39 -0
  58. package/src/model/stream/policy.ts +33 -0
  59. package/src/{agent/binding → model/stream}/request.ts +2 -2
  60. package/src/server/model-services.ts +3 -4
  61. package/src/tardie/projection/cost.ts +1 -0
  62. package/ui/assets/{index-CRr2FBVb.js → index-DHI0N7wi.js} +1 -1
  63. package/ui/index.html +1 -1
  64. package/src/agent/binding/continuation.ts +0 -18
  65. package/src/agent/binding/policy.ts +0 -13
  66. package/src/model/binding/continuation.ts +0 -1
  67. package/src/model/binding/output.ts +0 -1
  68. package/src/model/binding/prompt.ts +0 -1
  69. package/src/model/binding/response.ts +0 -1
  70. package/src/model/inference/observer.ts +0 -1
  71. package/src/model/inference/policy.ts +0 -1
  72. package/src/model/inference/request.ts +0 -1
  73. package/src/model/providers/response.ts +0 -1
  74. /package/src/{agent/binding → model/stream}/collect.ts +0 -0
  75. /package/src/model/{binding/usage.ts → usage.ts} +0 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tardie",
3
- "version": "0.26.0-rc.0",
3
+ "version": "0.26.0",
4
4
  "license": "MIT",
5
5
  "author": "Clavia, Inc.",
6
6
  "description": "Actor, agent, client, and host APIs for Tardigrade.",
@@ -16,7 +16,7 @@
16
16
  "access": "public"
17
17
  },
18
18
  "tardigrade": {
19
- "sourceTree": "bedab20bae90c0fc7522a8ed56b9303298828193"
19
+ "sourceTree": "055f74c19df551861b617c0df419d9496438966b"
20
20
  },
21
21
  "files": [
22
22
  "src",
@@ -97,8 +97,8 @@
97
97
  "./model/metadata": "./src/model/catalog/metadata.ts",
98
98
  "./model/directory": "./src/model/providers/directory.ts",
99
99
  "./model/reasoning": "./src/model/providers/options.ts",
100
- "./model/request-policy": "./src/model/inference/policy.ts",
101
- "./model/output": "./src/model/binding/output.ts",
100
+ "./model/request-policy": "./src/model/stream/policy.ts",
101
+ "./model/output": "./src/model/output.ts",
102
102
  "./model/*": "./src/model/*.ts"
103
103
  },
104
104
  "dependencies": {
@@ -108,16 +108,17 @@
108
108
  "@effect/sql-sqlite-bun": "4.0.0-rc.115",
109
109
  "@effect/sql-sqlite-do": "4.0.0-rc.115",
110
110
  "@tardie/ai": "0.0.2",
111
- "@tardie/ai-anthropic": "4.0.0-rc.113-clavia.1",
112
- "@tardie/ai-openai": "4.0.0-rc.113-clavia.2",
113
- "@tardie/ai-openai-compat": "4.0.0-rc.113-clavia.1",
111
+ "@tardie/ai-anthropic": "4.0.0-rc.113-clavia.3",
112
+ "@tardie/ai-openai": "4.0.0-rc.113-clavia.4",
113
+ "@tardie/ai-openai-compat": "4.0.0-rc.113-clavia.4",
114
+ "@tardie/ai-openrouter": "4.0.0-rc.113-clavia.1",
114
115
  "effect": "4.0.0-rc.115",
115
116
  "jsonc-parser": "3.3.1"
116
117
  },
117
118
  "peerDependencies": {
118
119
  "@smithy/fetch-http-handler": "^5.6.3",
119
120
  "@smithy/node-http-handler": "^4.11.2",
120
- "@tardie/ai-bedrock": "0.0.1"
121
+ "@tardie/ai-bedrock": "0.0.3"
121
122
  },
122
123
  "peerDependenciesMeta": {
123
124
  "@smithy/fetch-http-handler": {
@@ -0,0 +1,32 @@
1
+ import { Effect, Option } from "effect"
2
+ import { Toolkit, type Response } from "effect/unstable/ai"
3
+ import { FetchHttpClient } from "effect/unstable/http"
4
+ import { collectResponse } from "tardie/model/stream/collect"
5
+ import { observeResponse } from "tardie/model/stream/delivery"
6
+ import { BindingSettings, CurrentModel, ProviderRequestKey } from "tardie/model/settings"
7
+ import type { InferenceIdentity } from "tardie/model/stream/observer"
8
+ import type { ModelRef } from "tardie/model/reference"
9
+ import { unknownModelError } from "tardie/model/error"
10
+
11
+ // summarize accepts a nonempty completed summary without tool calls (component/compaction.test.ts).
12
+ export const summarize = (prompt: string, identity: InferenceIdentity, model?: ModelRef) => Effect.gen(function* () {
13
+ const settings = yield* BindingSettings
14
+ const observer = yield* observeResponse(identity, model ?? { provider: settings.provider, model_id: settings.model }, identity.turn, settings.observer)
15
+ const transport = Option.getOrElse(yield* Effect.serviceOption(FetchHttpClient.RequestInit), () => ({}))
16
+ const fetchOptions = { ...transport, timeout: false }
17
+ const response = yield* collectResponse(prompt, Toolkit.make(), observer.onPart, undefined, settings.policy.timeout).pipe(
18
+ Effect.provideService(CurrentModel, model),
19
+ Effect.provideService(ProviderRequestKey, identity.turn),
20
+ Effect.provideService(FetchHttpClient.RequestInit, fetchOptions),
21
+ Effect.ensuring(observer.finish)
22
+ )
23
+ const parts: ReadonlyArray<Response.AnyPart> = response.parts
24
+ const error = parts.find(part => part.type === "error")
25
+ if (error?.type === "error") return yield* unknownModelError(error.error)
26
+ const finish = parts.findLast(part => part.type === "finish")
27
+ const text = parts.flatMap(part => part.type === "text-delta" ? [part.delta] : []).join("")
28
+ if ((finish?.reason !== "stop" && finish?.reason !== "tool-calls") || parts.some(part => part.type === "tool-call") || text.trim() === "") {
29
+ return yield* unknownModelError("Compaction requires a nonempty completed summary without tool calls")
30
+ }
31
+ return text
32
+ })
@@ -1,6 +1,6 @@
1
1
  import { contextPolicyOf, resolvedContextPolicyOf, checkpointOf, keepFromIndex, type ContextPolicy, type CompactionPolicy } from "./context"
2
2
  export { contextPolicyOf, resolvedContextPolicyOf, checkpointOf, keepFromIndex, suffixOf, DEFAULT_COMPACTION_POLICY, type ContextPolicy, type ContextWindowTokens, type CompactionPolicy } from "./context"
3
- import { replayOf } from "../binding/continuation"
3
+ import { replayOf } from "../inference/model/continuation"
4
4
  import { renderMessageEntries } from "../projection/messages"
5
5
  import { upcastError } from "../log/upcast"
6
6
  import { hasUnansweredToolCall, responsesOf } from "../log/response"
@@ -25,8 +25,8 @@ import {
25
25
  type TranscriptProjectionState
26
26
  } from "../projection/transcript"
27
27
  import { LanguageModel } from "effect/unstable/ai"
28
- import { react } from "../binding/index"
29
- import { BindingSettings, ModelSelection } from "../binding/settings"
28
+ import { summarize } from "./compaction/model"
29
+ import { BindingSettings, ModelSelection } from "tardie/model/settings"
30
30
  import { modelRefOf, type ModelRef } from "../inference/reference"
31
31
  import type { AgentComponent } from "../runtime/composition"
32
32
 
@@ -70,8 +70,8 @@ const renderedWeights = (events: ReadonlyArray<Event>, policy: ContextPolicy, mo
70
70
  for (const { event, message } of renderMessageEntries(events, policy)) {
71
71
  const continuation = message.continuation
72
72
  const replay = continuation === undefined ? undefined : replayOf(continuation, model === undefined ? continuation : { ...continuation, provider: model.provider, model: model.model_id })
73
- const chars = replay?.messages === undefined
74
- ? (message.content?.length ?? 0) + (message.toolCalls ?? []).reduce((sum, call) => sum + call.arguments.length, 0) + (replay?.reasoning ?? []).reduce((sum, text) => sum + text.length, 0)
73
+ const chars = replay === undefined
74
+ ? (message.content?.length ?? 0) + (message.toolCalls ?? []).reduce((sum, call) => sum + call.arguments.length, 0)
75
75
  : JSON.stringify(continuation!.payload).length
76
76
  weights.set(event, (weights.get(event) ?? 0) + chars)
77
77
  }
@@ -237,22 +237,13 @@ const compactionTransition = (
237
237
  const summaryModel = input.model === undefined
238
238
  ? undefined
239
239
  : selection.resolve?.(input.model).model ?? input.model
240
- const action = yield* react(
241
- {
242
- trajectory: [{ type: "MessageReceived", id: `compact-${input.keepFrom}`, text: brief, at }],
243
- identity: { ...self, turn: `compact-${input.keepFrom}` },
244
- ...(summaryModel === undefined ? {} : { model: summaryModel }),
245
- system: "",
246
- tools: []
247
- },
248
- `compact-${input.keepFrom}`
249
- ).pipe(Effect.provideService(BindingSettings, yield* (selection.settings?.(summaryModel) ?? BindingSettings)))
250
- if (action.kind !== "complete" || action.output.trim() === "") {
251
- return yield* Effect.die(action.kind === "fail" ? action.error : new Error("Compaction requires a nonempty summary"))
252
- }
240
+ const summary = yield* summarize(brief, { ...self, turn: `compact-${input.keepFrom}` }, summaryModel).pipe(
241
+ Effect.provideService(BindingSettings, yield* (selection.settings?.(summaryModel) ?? BindingSettings)),
242
+ Effect.orDie
243
+ )
253
244
  return [compactionCompleted({
254
245
  keepFrom: input.keepFrom,
255
- summary: action.output,
246
+ summary,
256
247
  contextWindowTokens: input.contextWindowTokens,
257
248
  fireTokens: input.fireTokens,
258
249
  keepTokens: input.keepTokens,
@@ -1,184 +1 @@
1
- import { Schema } from "effect"
2
- import { ModelRef, modelRefOf, type ModelRef as ModelRefType } from "./reference"
3
-
4
- const PolicyName = Schema.String.check(Schema.isTrimmed(), Schema.isNonEmpty())
5
-
6
- export const ModelSelector = Schema.Struct({
7
- provider: PolicyName,
8
- model_ids: Schema.Union([Schema.Literal("*"), Schema.Array(PolicyName).check(Schema.isNonEmpty())])
9
- })
10
-
11
- export interface ModelSelector {
12
- readonly provider: string
13
- readonly model_ids: "*" | ReadonlyArray<string>
14
- }
15
-
16
- export const ModelAllow = Schema.Union([Schema.Literal("*"), Schema.Array(ModelSelector)])
17
- export type ModelAllow = "*" | ReadonlyArray<ModelSelector>
18
-
19
- // ModelPolicy carries resolved coordinate authority. Future rule languages lower into this selector form before composition, so intersection remains the authority boundary.
20
- export const ModelPolicy = Schema.Struct({
21
- default: Schema.optional(ModelRef),
22
- allow: ModelAllow
23
- })
24
-
25
- export interface ModelPolicy {
26
- readonly default?: ModelRefType
27
- readonly allow: ModelAllow
28
- }
29
-
30
- export const ModelPolicyOverride = Schema.Struct({
31
- default: Schema.optional(ModelRef),
32
- allow: Schema.optional(ModelAllow)
33
- })
34
-
35
- export interface ModelPolicyOverride {
36
- readonly default?: ModelRefType
37
- readonly allow?: ModelAllow
38
- }
39
-
40
- // DEFAULT_MODEL_POLICY leaves model authority unchanged and selects no default for an unconfigured host.
41
- export const DEFAULT_MODEL_POLICY: ModelPolicy = { allow: "*" }
42
-
43
- // DEFAULT_MODEL_POLICY_OVERRIDE inherits model authority and its default.
44
- export const DEFAULT_MODEL_POLICY_OVERRIDE: ModelPolicyOverride = {}
45
-
46
- const recordOf = (value: unknown): Record<string, unknown> | undefined =>
47
- typeof value === "object" && value !== null && !Array.isArray(value) ? value as Record<string, unknown> : undefined
48
-
49
- const textOf = (value: unknown): string | undefined =>
50
- typeof value === "string" && value.trim().length > 0 ? value.trim() : undefined
51
-
52
- const selectorOf = (value: unknown, index: number): ModelSelector => {
53
- const selector = recordOf(value)
54
- if (selector === undefined) throw new Error(`model selector ${index} must be an object`)
55
- const unknown = Object.keys(selector).filter((field) => field !== "provider" && field !== "model_ids")
56
- if (unknown.length > 0) throw new Error(`model selector ${index} contains unknown fields: ${unknown.join(", ")}`)
57
- const provider = textOf(selector["provider"])
58
- if (provider === undefined) throw new Error(`model selector ${index} must declare provider`)
59
- const rawModels = selector["model_ids"]
60
- if (rawModels === "*") return { provider, model_ids: "*" }
61
- if (!Array.isArray(rawModels) || rawModels.length === 0) {
62
- throw new Error(`model selector ${index} model_ids must be "*" or a non-empty array`)
63
- }
64
- const modelIds = rawModels.map((value, modelIndex) => {
65
- const model = textOf(value)
66
- if (model === undefined) throw new Error(`model selector ${index} model_ids[${modelIndex}] must be a non-empty string`)
67
- return model
68
- })
69
- return { provider, model_ids: [...new Set(modelIds)].sort() }
70
- }
71
-
72
- type ProviderModels = "*" | Set<string>
73
-
74
- // selectorMapOf lowers the current selector language into the set representation consumed by policy intersection. Query resolvers add syntax before this boundary and persist the normalized result.
75
- const selectorMapOf = (selectors: ReadonlyArray<ModelSelector>): Map<string, ProviderModels> => {
76
- const providers = new Map<string, ProviderModels>()
77
- for (const selector of selectors) {
78
- const current = providers.get(selector.provider)
79
- if (current === "*" || selector.model_ids === "*") {
80
- providers.set(selector.provider, "*")
81
- } else {
82
- providers.set(selector.provider, new Set([...(current ?? []), ...selector.model_ids]))
83
- }
84
- }
85
- return providers
86
- }
87
-
88
- const selectorsOf = (providers: ReadonlyMap<string, ProviderModels>): ReadonlyArray<ModelSelector> =>
89
- [...providers.entries()]
90
- .sort(([left], [right]) => left.localeCompare(right))
91
- .map(([provider, models]) => ({ provider, model_ids: models === "*" ? "*" : [...models].sort() }))
92
-
93
- const allowOf = (rawAllow: unknown, required: boolean): ModelAllow | undefined => {
94
- if (rawAllow === "*") return "*"
95
- if (Array.isArray(rawAllow)) return selectorsOf(selectorMapOf(rawAllow.map(selectorOf)))
96
- if (!required && rawAllow === undefined) return undefined
97
- throw new Error(`model policy allow must be "*" or an array`)
98
- }
99
-
100
- // modelAllowedBy reports whether one reference belongs to a policy's selected set.
101
- export const modelAllowedBy = (policy: ModelPolicy, reference: ModelRefType): boolean =>
102
- policy.allow === "*" || policy.allow.some((selector) =>
103
- selector.provider === reference.provider &&
104
- (selector.model_ids === "*" || selector.model_ids.includes(reference.model_id))
105
- )
106
-
107
- // modelPolicyOf validates one policy and gives duplicate selectors a stable representation.
108
- export const modelPolicyOf = (value: unknown): ModelPolicy => {
109
- if (value === undefined) return DEFAULT_MODEL_POLICY
110
- const policy = recordOf(value)
111
- if (policy === undefined) throw new Error("model policy must be an object")
112
- const unknown = Object.keys(policy).filter((field) => field !== "default" && field !== "allow")
113
- if (unknown.length > 0) throw new Error(`model policy contains unknown fields: ${unknown.join(", ")}`)
114
- if (!("allow" in policy)) throw new Error('model policy must declare allow as "*" or an array')
115
- const rawDefault = policy["default"]
116
- const selected = modelRefOf(rawDefault)
117
- if (rawDefault !== undefined && selected === undefined) throw new Error("model policy default must be { provider, model_id }")
118
- const allow = allowOf(policy["allow"], true)!
119
- const normalized: ModelPolicy = { ...(selected === undefined ? {} : { default: selected }), allow }
120
- if (selected !== undefined && !modelAllowedBy(normalized, selected)) {
121
- throw new Error(`model policy default ${selected.provider}/${selected.model_id} is excluded by allow`)
122
- }
123
- return normalized
124
- }
125
-
126
- // modelPolicyOverrideOf validates an optional downstream attenuation and default override.
127
- export const modelPolicyOverrideOf = (value: unknown): ModelPolicyOverride => {
128
- if (value === undefined) return DEFAULT_MODEL_POLICY_OVERRIDE
129
- const policy = recordOf(value)
130
- if (policy === undefined) throw new Error("model policy override must be an object")
131
- const unknown = Object.keys(policy).filter((field) => field !== "default" && field !== "allow")
132
- if (unknown.length > 0) throw new Error(`model policy override contains unknown fields: ${unknown.join(", ")}`)
133
- const rawDefault = policy["default"]
134
- const selected = modelRefOf(rawDefault)
135
- if (rawDefault !== undefined && selected === undefined) throw new Error("model policy override default must be { provider, model_id }")
136
- const allow = allowOf(policy["allow"], false)
137
- const normalized: ModelPolicyOverride = {
138
- ...(selected === undefined ? {} : { default: selected }),
139
- ...(allow === undefined ? {} : { allow })
140
- }
141
- if (selected !== undefined && allow !== undefined && !modelAllowedBy({ allow }, selected)) {
142
- throw new Error(`model policy override default ${selected.provider}/${selected.model_id} is excluded by allow`)
143
- }
144
- return normalized
145
- }
146
-
147
- const intersectModels = (left: ProviderModels, right: ProviderModels): ProviderModels => {
148
- if (left === "*") return right === "*" ? "*" : new Set(right)
149
- if (right === "*") return new Set(left)
150
- return new Set([...left].filter((model) => right.has(model)))
151
- }
152
-
153
- // intersectModelPolicies returns the authority shared by every layer (packages/core/tla/component/ModelPolicy.tla, ChildCannotWiden; HostCeiling).
154
- export const intersectModelPolicies = (policies: ReadonlyArray<ModelPolicy>): ModelPolicy => {
155
- const explicit = policies.filter((policy) => policy.allow !== "*")
156
- if (explicit.length === 0) return DEFAULT_MODEL_POLICY
157
- let intersection = selectorMapOf(explicit[0]!.allow as ReadonlyArray<ModelSelector>)
158
- for (const policy of explicit.slice(1)) {
159
- const right = selectorMapOf(policy.allow as ReadonlyArray<ModelSelector>)
160
- const next = new Map<string, ProviderModels>()
161
- for (const [provider, models] of intersection) {
162
- const other = right.get(provider)
163
- if (other === undefined) continue
164
- const shared = intersectModels(models, other)
165
- if (shared === "*" || shared.size > 0) next.set(provider, shared)
166
- }
167
- intersection = next
168
- }
169
- return { allow: selectorsOf(intersection) }
170
- }
171
-
172
- // applyModelPolicy attenuates incoming authority and inherits or overrides its default (packages/core/tla/component/ModelPolicy.tla, ChildCannotWiden; DefaultAllowed).
173
- export const applyModelPolicy = (incoming: ModelPolicy, override: ModelPolicyOverride): ModelPolicy => {
174
- const authority = intersectModelPolicies([incoming, { allow: override.allow ?? "*" }])
175
- const selected = override.default ?? incoming.default
176
- if (selected !== undefined && !modelAllowedBy(authority, selected)) {
177
- throw new Error(`effective model policy excludes default ${selected.provider}/${selected.model_id}; supply an allowed default`)
178
- }
179
- return { ...authority, ...(selected === undefined ? {} : { default: selected }) }
180
- }
181
-
182
- // modelPolicyScopeOf returns a stable cursor scope for a policy intersection.
183
- export const modelPolicyScopeOf = (policies: ReadonlyArray<ModelPolicy>): string =>
184
- JSON.stringify(intersectModelPolicies(policies).allow)
1
+ export * from "tardie/model/access"
@@ -3,7 +3,7 @@ import type { Event } from "tardie/core/log/event"
3
3
  import type { ContextPolicy } from "../component/compaction"
4
4
  import type { OutputFallback } from "../output/contract"
5
5
  import type { ModelRef } from "./reference"
6
- import { DEFAULT_MODEL_POLICY_OVERRIDE, type ModelPolicy, type ModelPolicyOverride } from "./access"
6
+ import { DEFAULT_MODEL_POLICY_OVERRIDE, type ModelPolicyOverride } from "./access"
7
7
  import type { InferenceIdentity } from "./observer"
8
8
 
9
9
  // InferPolicy states the process-crash ceiling and model authority applied by the inference machine. Output correction bounds belong to the mounted output component (component/repair.ts, RepairPolicy).
@@ -26,11 +26,7 @@ export interface InferRequest {
26
26
  readonly output?: { readonly fallback: OutputFallback; readonly system?: string }
27
27
  }
28
28
 
29
- export interface ModelResolution {
30
- readonly model: ModelRef
31
- // models is the interpreter's current authority for validating this call. It is not recorded.
32
- readonly models?: ModelPolicy
33
- }
29
+ export type { ModelResolution } from "tardie/model/reference"
34
30
 
35
31
  // NativeOutputSupport declares support for native structured output beside tools (component/native-output.ts).
36
32
  export class NativeOutputSupport extends Context.Service<
@@ -1,36 +1 @@
1
- import { Schema } from "effect"
2
- import { AiError } from "effect/unstable/ai"
3
-
4
- export const ModelError = Schema.toCodecJson(AiError.AiError)
5
- export type ModelError = typeof ModelError.Type
6
-
7
- // encodeModelError preserves JSON duration encoding and native HTTP redaction (error.test.ts, packages/model/src/binding/errors.test.ts).
8
- export const encodeModelError = (error: AiError.AiError): Schema.Json => {
9
- const object = Schema.Record(Schema.String, Schema.Json)
10
- const encoded = Schema.decodeUnknownSync(object)(Schema.encodeSync(ModelError)(error))
11
- const reason = encoded.reason
12
- if ("http" in error.reason && error.reason.http !== undefined && Schema.is(object)(reason)) {
13
- const http = Schema.decodeUnknownSync(Schema.Json)(
14
- JSON.parse(Schema.encodeSync(Schema.fromJsonString(AiError.HttpContext))(error.reason.http))
15
- )
16
- return { ...encoded, reason: { ...Schema.decodeSync(object)(reason), http } }
17
- }
18
- return encoded
19
- }
20
-
21
- // unknownModelError preserves serializable evidence from unclassified failures.
22
- export const unknownModelError = (cause: unknown): AiError.AiError => {
23
- if (AiError.isAiError(cause)) return cause
24
- const description = cause instanceof Error ? cause.message || cause.name : typeof cause === "string" ? cause : typeof cause === "object" && cause !== null && "message" in cause && typeof cause.message === "string" ? cause.message : "Model inference failed"
25
- const evidence = cause instanceof Error
26
- ? { ...Object.fromEntries(Object.entries(cause).filter(([, value]) => Schema.is(Schema.Json)(value))), name: cause.name, message: cause.message, ...(cause.stack === undefined ? {} : { stack: cause.stack }) }
27
- : Schema.is(Schema.Json)(cause) ? cause : String(cause)
28
- return AiError.make({ module: "Tardigrade", method: "inference", reason: AiError.UnknownError.make({ description, metadata: { tardigrade: { evidence } } }) })
29
- }
30
-
31
- // modelErrorOf decodes persisted native evidence; historical envelopes remain historical.
32
- export const modelErrorOf = (error: unknown): AiError.AiError | undefined => {
33
- if (AiError.isAiError(error)) return error
34
- const decoded = Schema.decodeUnknownOption(ModelError)(error)
35
- return decoded._tag === "Some" ? decoded.value : undefined
36
- }
1
+ export * from "tardie/model/error"
@@ -4,9 +4,9 @@ import { bindTransitionContext } from "tardie/core/transition/transition"
4
4
  import { eventAt, eventPositionOf } from "tardie/core/event"
5
5
  import { RetrySchedule, retryDelayOf } from "./retry"
6
6
  import { LanguageModel } from "effect/unstable/ai"
7
- import { react } from "../binding/index"
7
+ import { react } from "./model/index"
8
8
  import { unknownModelError } from "./error"
9
- import { BindingSettings, ModelSelection } from "../binding/settings"
9
+ import { BindingSettings, ModelSelection } from "./model/settings"
10
10
  import { Cause, Clock, Effect, Random, Schema } from "effect"
11
11
  import { EventLog } from "tardie/core/log"
12
12
  import { HashMap, Option } from "effect"
@@ -0,0 +1,14 @@
1
+ import { Schema } from "effect"
2
+ import { Prompt } from "effect/unstable/ai"
3
+ import type { ProviderContinuation } from "../continuation"
4
+
5
+ export interface ReplayIdentity {
6
+ readonly provider: string
7
+ readonly protocol: string
8
+ readonly model: string
9
+ }
10
+
11
+ export const replayOf = (continuation: ProviderContinuation | undefined, identity: ReplayIdentity): ReadonlyArray<Prompt.Message> | undefined => {
12
+ if (continuation === undefined || continuation.provider !== identity.provider || continuation.protocol !== identity.protocol || continuation.model !== identity.model) return undefined
13
+ return Schema.decodeSync(Prompt.Prompt)(continuation.payload).content
14
+ }
@@ -1,19 +1,19 @@
1
1
  import { historyOf, importSchema } from "./prompt"
2
2
  import { actionOf, responseEvidence, errorOf } from "./response"
3
- import { observerPolicyOf, deltaDelivery } from "./observer"
4
- import type { InferDelta } from "../inference/observer"
5
- import type { InferRequest } from "../inference/contract"
3
+ import { observeResponse } from "tardie/model/stream/delivery"
4
+ import type { InferDelta } from "../observer"
5
+ import type { InferRequest } from "../contract"
6
6
  import { FetchHttpClient } from "effect/unstable/http"
7
7
  import { Duration, Effect, Option } from "effect"
8
- import { AiError, IdGenerator, Prompt, Response, Tool, Toolkit } from "effect/unstable/ai"
9
- import { modelRequest } from "../inference/request"
10
- import type { Action } from "../log/events"
11
- import { collectResponse } from "./collect"
8
+ import { AiError, Prompt, Response, Tool, Toolkit } from "effect/unstable/ai"
9
+ import { modelRequest } from "../request"
10
+ import type { Action } from "../../log/events"
11
+ import { collectResponse } from "tardie/model/stream/collect"
12
12
  import { BindingSettings, BindingInvocation, CurrentModel, ProviderRequestKey } from "./settings"
13
13
 
14
14
  import { fallbackSystemFor, outputModeOf, outputSchemaFor, outputNameFor } from "./output"
15
15
 
16
- import { StreamIncomplete, StreamBoundExceeded, StreamTruncated } from "./request"
16
+ import { StreamIncomplete, StreamBoundExceeded, StreamTruncated } from "tardie/model/stream/request"
17
17
 
18
18
  // react translates one Effect response into an action for the inference machine.
19
19
  export const react = (request: InferRequest, key?: string, signal?: AbortSignal, onDelta?: (delta: InferDelta) => void) => Effect.gen(function* () {
@@ -21,9 +21,8 @@ export const react = (request: InferRequest, key?: string, signal?: AbortSignal,
21
21
  const { provider: providerId, protocol, policy } = options
22
22
  const endpoint = { provider: providerId, model: request.model?.model_id ?? options.model }
23
23
  const outputPolicy = { ...endpoint, ...(options.output === undefined ? {} : { output: options.output }) }
24
- const observerPolicy = options.observer === undefined ? undefined : observerPolicyOf(options.observer)
25
24
  if (signal?.aborted) return yield* Effect.interrupt
26
- const delivery = options.observer === undefined || observerPolicy === undefined ? undefined : yield* deltaDelivery(options.observer, observerPolicy)
25
+ const delivery = yield* observeResponse(request.identity, { provider: providerId, model_id: endpoint.model }, key, options.observer, onDelta)
27
26
  return yield* Effect.suspend(() => {
28
27
  let reportedCostUsd: number | undefined
29
28
  let observed: Response.AnyPart[] = []
@@ -55,18 +54,10 @@ export const react = (request: InferRequest, key?: string, signal?: AbortSignal,
55
54
  const response = yield* Effect.gen(function* () {
56
55
  const maxOutputTokens = policy.maxOutputTokens
57
56
  observed = []
58
- let sequence = 0
59
- let blockIndex = -1
60
- const physicalAttempt = yield* IdGenerator.defaultIdGenerator.generateId()
61
57
  const result = yield* collectResponse(Prompt.setSystem(Prompt.fromMessages(history), system), Toolkit.make(...tools), (part) => {
62
58
  observed.push(part)
63
59
  if (part.type === "finish") reportedCostUsd = options.reportedCostUsd?.(part)
64
- if (part.type === "text-start" || part.type === "reasoning-start") blockIndex += 1
65
- if (part.type === "text-delta" || part.type === "reasoning-delta") {
66
- const delta: InferDelta = { ...request.identity, logicalAttempt: key ?? request.identity.turn, physicalAttempt, model: { provider: providerId, model_id: endpoint.model }, blockIndex: Math.max(0, blockIndex), sequence: sequence++, text: part.delta, ...(part.type === "reasoning-delta" ? { kind: "reasoning" as const } : {}) }
67
- onDelta?.(delta)
68
- return delivery?.offer(delta).pipe(Effect.asVoid)
69
- }
60
+ return delivery.onPart(part)
70
61
  }, responseFormat, policy.timeout)
71
62
  if (result.parts.some((part) => part.type === "finish" && part.reason === "length")) return yield* new StreamTruncated({ maxOutputTokens })
72
63
  return result
@@ -108,5 +99,5 @@ export const react = (request: InferRequest, key?: string, signal?: AbortSignal,
108
99
  } satisfies Action
109
100
  })),
110
101
  Effect.map((action): Action => reportedCostUsd === undefined ? action : { ...action, reportedCostUsd }))
111
- }).pipe(Effect.ensuring(delivery?.finish ?? Effect.void))
102
+ }).pipe(Effect.ensuring(delivery.finish))
112
103
  })
@@ -1,18 +1,13 @@
1
- import { NATIVE_MODE, outputNameErrors, outputProfileErrors, type OutputMode } from "../output/contract"
2
- import type { OutputRequest } from "../inference/request"
1
+ import { NATIVE_MODE, outputNameErrors, outputProfileErrors, type OutputMode } from "../../output/contract"
2
+ import type { OutputRequest } from "../request"
3
3
 
4
4
  // The binding's half of the output contract: what an endpoint promises about a declared schema,
5
5
  // what that promise costs a request, and how the schema reaches each wire. A turn that declares a
6
6
  // contract the configured endpoint cannot honour fails here, before a socket opens
7
7
  // (binding/index.test.ts).
8
8
 
9
- // OutputCapability is what one configured endpoint promises about a declared contract. It is a
10
- // union so a value cannot say two things at once: an endpoint that promises nothing has no
11
- // tool-combination question to answer, and one that promises a native strict schema must say
12
- // whether that schema may ride the same call as a tool list.
13
- export type OutputCapability =
14
- | { readonly guarantee: "none" }
15
- | { readonly guarantee: "native"; readonly withTools: boolean }
9
+ import type { OutputCapability } from "tardie/model/output"
10
+ export type { OutputCapability } from "tardie/model/output"
16
11
 
17
12
  // UNPROVEN is the message an endpoint that declared nothing earns. It names the two ways out, so
18
13
  // an operator reading a failed turn knows what to set rather than which source to read.
@@ -1,6 +1,6 @@
1
1
  import { JsonSchema, Schema, SchemaRepresentation } from "effect"
2
2
  import { Prompt } from "effect/unstable/ai"
3
- import type { AgentMessage } from "../projection/messages"
3
+ import type { AgentMessage } from "../../projection/messages"
4
4
  import { replayOf, type ReplayIdentity } from "./continuation"
5
5
 
6
6
  export const DEFAULT_SCHEMA_IMPORT_OPTIONS = { patterns: "apply" } as const satisfies SchemaRepresentation.FromJsonSchemaOptions
@@ -11,18 +11,16 @@ export const historyOf = (messages: ReadonlyArray<AgentMessage>, identity: Repla
11
11
  const names = new Map<string, string>()
12
12
  return messages.flatMap((message) => {
13
13
  for (const call of message.toolCalls ?? []) names.set(call.id, call.name)
14
- const replay = replayOf(message.continuation, identity)
15
- return replay.messages ?? promptMessage(message, names, replay.reasoning)
14
+ return replayOf(message.continuation, identity) ?? promptMessage(message, names)
16
15
  })
17
16
  }
18
17
 
19
- const promptMessage = (message: AgentMessage, names: ReadonlyMap<string, string>, reasoning: ReadonlyArray<string> = []): ReadonlyArray<Prompt.Message> => {
18
+ const promptMessage = (message: AgentMessage, names: ReadonlyMap<string, string>): ReadonlyArray<Prompt.Message> => {
20
19
  if (message.role === "user") return [Prompt.userMessage({ content: [Prompt.makePart("text", { text: message.content ?? "" })] })]
21
20
  if (message.role === "tool") return [Prompt.toolMessage({ content: [Prompt.makePart("tool-result", {
22
21
  id: message.toolCallId ?? "", name: names.get(message.toolCallId ?? "") ?? "", result: message.content ?? "", isFailure: message.isFailure ?? false, providerExecuted: false
23
22
  })] })]
24
23
  return [Prompt.assistantMessage({ content: [
25
- ...reasoning.map((text) => Prompt.makePart("text", { text })),
26
24
  ...(message.content ? [Prompt.makePart("text", { text: message.content })] : []),
27
25
  ...(message.toolCalls ?? []).map((call) => Prompt.makePart("tool-call", { id: call.id, name: call.name, params: JSON.parse(call.arguments), providerExecuted: false }))
28
26
  ] })]
@@ -1,8 +1,8 @@
1
1
  import { Effect, Schema } from "effect"
2
2
  import { AiError, Response } from "effect/unstable/ai"
3
- import { type Action, type ToolCall } from "../log/events"
4
- import { ToolCallValidationError } from "./collect"
5
- import { unknownModelError } from "../inference/error"
3
+ import { type Action, type ToolCall } from "../../log/events"
4
+ import { ToolCallValidationError } from "tardie/model/stream/collect"
5
+ import { unknownModelError } from "../error"
6
6
 
7
7
  /**
8
8
  * actionOf translates Effect response parts into the agent's Action contract.
@@ -40,7 +40,7 @@ export const actionOf = (parts: ReadonlyArray<Response.AnyPart>, served: Pick<Ac
40
40
  const calls: ToolCall[] = []
41
41
  const errors: AiError.AiError[] = []
42
42
  for (const part of parts) {
43
- if (part.type === "tool-call") calls.push({ callId: part.id, name: part.name, arguments: part.params })
43
+ if (part.type === "tool-call" && !part.providerExecuted) calls.push({ callId: part.id, name: part.name, arguments: part.params })
44
44
  if (part.type === "error") {
45
45
  if (!Schema.is(ToolCallValidationError)(part.error)) {
46
46
  errors.push(yield* errorOf(part.error))
@@ -0,0 +1,11 @@
1
+ import { Context } from "effect"
2
+ import type { InferRequest } from "../contract"
3
+ import type { InferDelta } from "../observer"
4
+ export { BindingSettings, CurrentModel, ModelSelection, ProviderRequestKey, type BindingOptions } from "tardie/model/settings"
5
+
6
+ export const BindingInvocation = Context.Reference<{
7
+ readonly request: InferRequest
8
+ readonly key?: string | undefined
9
+ readonly signal?: AbortSignal | undefined
10
+ readonly onDelta?: ((delta: InferDelta) => void) | undefined
11
+ } | undefined>("tardie/BindingInvocation", { defaultValue: () => undefined })
@@ -1,39 +1 @@
1
- import type { Effect } from "effect"
2
- import type { ModelRef } from "./reference"
3
-
4
- // InferenceIdentity identifies the actor turn that opened a logical model attempt (index.test.ts, "root and child inference requests carry their actor identity"). instance names the actor instance, so two sessions of one actor name stay distinguishable at the model seam (index.test.ts, "two host instances of one actor name carry distinct instance identities").
5
- export interface InferenceIdentity {
6
- readonly actor: string
7
- readonly instance: string
8
- readonly thread: string
9
- readonly turn: string
10
- }
11
-
12
- // InferDelta is ephemeral answer or reasoning text from one physical provider request. Sequence is zero-based within that request, so a consumer can detect a dropped delta (packages/model/src/host.test.ts).
13
- export interface InferDelta extends InferenceIdentity {
14
- readonly logicalAttempt: string
15
- readonly physicalAttempt: string
16
- readonly model: ModelRef
17
- readonly blockIndex: number
18
- readonly sequence: number
19
- readonly kind?: "text" | "reasoning"
20
- readonly text: string
21
- }
22
-
23
- // InferenceObserver receives ephemeral output outside the durable log. Failure and timeout discard that delivery without changing inference (packages/model/src/inference/observer.test.ts).
24
- export interface InferenceObserver {
25
- readonly onDelta: (delta: InferDelta) => Effect.Effect<void, Error>
26
- readonly policy?: Partial<InferenceObserverPolicy>
27
- }
28
-
29
- // InferenceObserverPolicy bounds pending deliveries and each observer call.
30
- export interface InferenceObserverPolicy {
31
- readonly bufferCapacity: number
32
- readonly deliveryTimeoutMs: number
33
- }
34
-
35
- // DEFAULT_INFERENCE_OBSERVER_POLICY bounds best-effort delivery while each observer may override both fields.
36
- export const DEFAULT_INFERENCE_OBSERVER_POLICY: InferenceObserverPolicy = {
37
- bufferCapacity: 64,
38
- deliveryTimeoutMs: 100
39
- }
1
+ export * from "tardie/model/stream/observer"
@@ -1,18 +1 @@
1
- import { Schema } from "effect"
2
-
3
- // ModelRef identifies the provider and model an actor requests. The host supplies the provider connection.
4
- export const ModelRef = Schema.Struct({
5
- provider: Schema.NonEmptyString,
6
- model_id: Schema.NonEmptyString
7
- })
8
-
9
- export type ModelRef = typeof ModelRef.Type
10
-
11
- // modelRefOf reads a complete model reference from an untyped message or tool argument.
12
- export const modelRefOf = (value: unknown): ModelRef | undefined => {
13
- if (typeof value !== "object" || value === null) return undefined
14
- const reference = value as { readonly provider?: unknown; readonly model_id?: unknown }
15
- if (typeof reference.provider !== "string" || reference.provider.trim().length === 0) return undefined
16
- if (typeof reference.model_id !== "string" || reference.model_id.trim().length === 0) return undefined
17
- return { provider: reference.provider.trim(), model_id: reference.model_id.trim() }
18
- }
1
+ export * from "tardie/model/reference"