tardie 0.12.0 → 0.13.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/package.json +22 -6
- package/src/agent/components/compaction.ts +5 -3
- package/src/agent/index.ts +7 -0
- package/src/agent/inference/observer.ts +37 -0
- package/src/agent/inference/reactor.ts +11 -3
- package/src/cli/commands.ts +2 -1
- package/src/cli/init.ts +24 -3
- package/src/cloudflare/worker.ts +32 -6
- package/src/model/adapter.ts +104 -0
- package/src/model/anthropic.ts +29 -0
- package/src/model/bedrock.ts +158 -0
- package/src/model/model.ts +157 -240
- package/src/model/openai.ts +33 -0
- package/src/model/output.ts +0 -37
- package/src/server/host.ts +52 -7
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "tardie",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.13.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": "8db5a667a08511b951c0a6e341f585b76e77fb65"
|
|
20
20
|
},
|
|
21
21
|
"files": [
|
|
22
22
|
"src",
|
|
@@ -71,19 +71,35 @@
|
|
|
71
71
|
"./model/*": "./src/model/*.ts"
|
|
72
72
|
},
|
|
73
73
|
"dependencies": {
|
|
74
|
-
"@aws-sdk/client-bedrock-runtime": "^3.1079.0",
|
|
75
74
|
"@cfworker/json-schema": "^4.1.1",
|
|
76
75
|
"@effect/platform-bun": "4.0.0-rc.110",
|
|
77
76
|
"@effect/platform-node-shared": "4.0.0-rc.110",
|
|
78
77
|
"@effect/sql-sqlite-bun": "4.0.0-rc.110",
|
|
79
78
|
"@effect/sql-sqlite-do": "4.0.0-rc.110",
|
|
80
|
-
"@smithy/fetch-http-handler": "^5.6.3",
|
|
81
|
-
"@smithy/node-http-handler": "^4.11.2",
|
|
82
79
|
"@tanstack/ai": "0.46.0",
|
|
83
80
|
"@tanstack/ai-anthropic": "0.16.6",
|
|
84
|
-
"@tanstack/ai-bedrock": "0.2.3",
|
|
85
81
|
"@tanstack/ai-openai": "0.20.0",
|
|
86
82
|
"effect": "4.0.0-rc.110",
|
|
87
83
|
"jsonc-parser": "3.3.1"
|
|
84
|
+
},
|
|
85
|
+
"peerDependencies": {
|
|
86
|
+
"@aws-sdk/client-bedrock-runtime": "^3.1079.0",
|
|
87
|
+
"@smithy/fetch-http-handler": "^5.6.3",
|
|
88
|
+
"@smithy/node-http-handler": "^4.11.2",
|
|
89
|
+
"@tanstack/ai-bedrock": "0.2.3"
|
|
90
|
+
},
|
|
91
|
+
"peerDependenciesMeta": {
|
|
92
|
+
"@aws-sdk/client-bedrock-runtime": {
|
|
93
|
+
"optional": true
|
|
94
|
+
},
|
|
95
|
+
"@smithy/fetch-http-handler": {
|
|
96
|
+
"optional": true
|
|
97
|
+
},
|
|
98
|
+
"@smithy/node-http-handler": {
|
|
99
|
+
"optional": true
|
|
100
|
+
},
|
|
101
|
+
"@tanstack/ai-bedrock": {
|
|
102
|
+
"optional": true
|
|
103
|
+
}
|
|
88
104
|
}
|
|
89
105
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Clock, Effect } from "effect"
|
|
2
|
-
import { effect, type Reactor } from "tardie/core/reconciliation"
|
|
2
|
+
import { effect, Self, type Reactor } from "tardie/core/reconciliation"
|
|
3
3
|
import { compactionCompleted } from "../log/events"
|
|
4
4
|
import type { Event } from "tardie/core/log/event"
|
|
5
5
|
import { turnOf, turnView } from "tardie/code/execution/turns"
|
|
@@ -346,7 +346,7 @@ const firedUncovered = (log: ReadonlyArray<Event>): boolean => {
|
|
|
346
346
|
//
|
|
347
347
|
// The policy this takes must be the one the render takes, or the guard measures a request the
|
|
348
348
|
// model never sees (ContextPolicy above).
|
|
349
|
-
export const compactionReactor = (policy: Partial<CompactionPolicy> = {}): Reactor<Infer> => (log) => {
|
|
349
|
+
export const compactionReactor = (policy: Partial<CompactionPolicy> = {}): Reactor<Infer | Self> => (log) => {
|
|
350
350
|
const model = modelResolutionOf(log).model
|
|
351
351
|
const resolved = contextPolicyFrom(log, policy)
|
|
352
352
|
// The projection runs first, so the guard, the cut, and the brief all read the history the
|
|
@@ -371,6 +371,7 @@ export const compactionReactor = (policy: Partial<CompactionPolicy> = {}): React
|
|
|
371
371
|
},
|
|
372
372
|
act: (input) =>
|
|
373
373
|
Effect.gen(function* () {
|
|
374
|
+
const self = yield* Self
|
|
374
375
|
const at = yield* Clock.currentTimeMillis
|
|
375
376
|
const lines = input.span.map((e) => lineOf(e, resolved)).filter((l): l is string => l !== null)
|
|
376
377
|
if (lines.length === 0) {
|
|
@@ -392,6 +393,7 @@ export const compactionReactor = (policy: Partial<CompactionPolicy> = {}): React
|
|
|
392
393
|
const action = yield* (yield* Infer).react(
|
|
393
394
|
{
|
|
394
395
|
trajectory: [{ type: "MessageReceived", id: `compact-${input.keepFrom}`, text: brief, at }],
|
|
396
|
+
identity: { ...self, turn: `compact-${input.keepFrom}` },
|
|
395
397
|
...(model === undefined ? {} : { model }),
|
|
396
398
|
system: "",
|
|
397
399
|
tools: []
|
|
@@ -414,7 +416,7 @@ export const compactionReactor = (policy: Partial<CompactionPolicy> = {}): React
|
|
|
414
416
|
|
|
415
417
|
// compaction derives one resolved context contribution and the transitions governed by the same
|
|
416
418
|
// model-relative policy.
|
|
417
|
-
export const compaction = (policy: Partial<CompactionPolicy> = {}): AgentComponent<Infer> => {
|
|
419
|
+
export const compaction = (policy: Partial<CompactionPolicy> = {}): AgentComponent<Infer | Self> => {
|
|
418
420
|
const reactor = compactionReactor(policy)
|
|
419
421
|
return {
|
|
420
422
|
name: "compaction",
|
package/src/agent/index.ts
CHANGED
|
@@ -36,6 +36,13 @@ export {
|
|
|
36
36
|
type Render
|
|
37
37
|
} from "./inference/reactor"
|
|
38
38
|
export { ModelRef, modelRefOf } from "./inference/reference"
|
|
39
|
+
export {
|
|
40
|
+
DEFAULT_INFERENCE_OBSERVER_POLICY,
|
|
41
|
+
type InferDelta,
|
|
42
|
+
type InferenceIdentity,
|
|
43
|
+
type InferenceObserver,
|
|
44
|
+
type InferenceObserverPolicy
|
|
45
|
+
} from "./inference/observer"
|
|
39
46
|
export {
|
|
40
47
|
applyModelPolicy,
|
|
41
48
|
DEFAULT_MODEL_POLICY,
|
|
@@ -0,0 +1,37 @@
|
|
|
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").
|
|
5
|
+
export interface InferenceIdentity {
|
|
6
|
+
readonly actor: string
|
|
7
|
+
readonly thread: string
|
|
8
|
+
readonly turn: string
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
// InferDelta is ephemeral normalized text from one physical provider request. Sequence is zero-based within that request, so a consumer can detect a dropped delta (model.test.ts, "observes normalized text without changing the terminal action").
|
|
12
|
+
export interface InferDelta extends InferenceIdentity {
|
|
13
|
+
readonly logicalAttempt: string
|
|
14
|
+
readonly physicalAttempt: string
|
|
15
|
+
readonly model: ModelRef
|
|
16
|
+
readonly blockIndex: number
|
|
17
|
+
readonly sequence: number
|
|
18
|
+
readonly text: string
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// InferenceObserver receives ephemeral output outside the durable log. Failure and timeout discard that delivery without changing inference (model.test.ts, "observer failure and saturation leave inference unchanged").
|
|
22
|
+
export interface InferenceObserver {
|
|
23
|
+
readonly onDelta: (delta: InferDelta) => Effect.Effect<void, Error>
|
|
24
|
+
readonly policy?: Partial<InferenceObserverPolicy>
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// InferenceObserverPolicy bounds pending deliveries and each observer call.
|
|
28
|
+
export interface InferenceObserverPolicy {
|
|
29
|
+
readonly bufferCapacity: number
|
|
30
|
+
readonly deliveryTimeoutMs: number
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// DEFAULT_INFERENCE_OBSERVER_POLICY bounds best-effort delivery while each observer may override both fields.
|
|
34
|
+
export const DEFAULT_INFERENCE_OBSERVER_POLICY: InferenceObserverPolicy = {
|
|
35
|
+
bufferCapacity: 64,
|
|
36
|
+
deliveryTimeoutMs: 100
|
|
37
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Cause, Clock, Context, Effect } from "effect"
|
|
2
2
|
import { EventLog } from "tardie/core/log"
|
|
3
|
-
import { intent, effect, type Reactor } from "tardie/core/reconciliation"
|
|
3
|
+
import { intent, effect, Self, type Reactor } from "tardie/core/reconciliation"
|
|
4
4
|
import { modelCalled, modelResolved, outputRejected, textReturned, turnFailed } from "../log/events"
|
|
5
5
|
import type { Event } from "tardie/core/log/event"
|
|
6
6
|
import type { Action } from "../log/events"
|
|
@@ -28,6 +28,7 @@ import {
|
|
|
28
28
|
type ModelPolicy,
|
|
29
29
|
type ModelPolicyOverride
|
|
30
30
|
} from "./access"
|
|
31
|
+
import type { InferenceIdentity } from "./observer"
|
|
31
32
|
|
|
32
33
|
// The infer reactor: the model loop, and nothing else. A think is owed when the current turn
|
|
33
34
|
// has no unanswered tool call and no terminal; serving marks the attempt, does inference, then
|
|
@@ -51,6 +52,7 @@ export const DEFAULT_INFER_POLICY: InferPolicy = { giveUpAfter: 3, models: DEFAU
|
|
|
51
52
|
// about tools; the actor is the render's one owner.
|
|
52
53
|
export interface InferRequest {
|
|
53
54
|
readonly trajectory: ReadonlyArray<Event>
|
|
55
|
+
readonly identity: InferenceIdentity
|
|
54
56
|
readonly model?: ModelRef
|
|
55
57
|
readonly system: string
|
|
56
58
|
readonly tools: ReadonlyArray<import("./request").ToolSpec>
|
|
@@ -318,7 +320,7 @@ export type Render = (log: ReadonlyArray<Event>) => {
|
|
|
318
320
|
readonly output?: { readonly fallback: OutputFallback; readonly system?: string }
|
|
319
321
|
}
|
|
320
322
|
|
|
321
|
-
export const inferReactorFor = (policy: Partial<InferPolicy>, render: Render): Reactor<Infer | EventLog> => (log) => {
|
|
323
|
+
export const inferReactorFor = (policy: Partial<InferPolicy>, render: Render): Reactor<Infer | EventLog | Self> => (log) => {
|
|
322
324
|
const giveUpAfter = policy.giveUpAfter ?? DEFAULT_INFER_POLICY.giveUpAfter
|
|
323
325
|
const slice = turnView(log)
|
|
324
326
|
if (slice.length === 0 || awaitingTool(slice) || terminated(slice)) return []
|
|
@@ -514,6 +516,7 @@ export const inferReactorFor = (policy: Partial<InferPolicy>, render: Render): R
|
|
|
514
516
|
act: (input) =>
|
|
515
517
|
Effect.gen(function* () {
|
|
516
518
|
const events = yield* EventLog
|
|
519
|
+
const self = yield* Self
|
|
517
520
|
const at = yield* Clock.currentTimeMillis
|
|
518
521
|
// The mark records the attempt BEFORE the inference, appended by the act itself: a
|
|
519
522
|
// died attempt leaves its mark, the next derivation counts it, the bound holds.
|
|
@@ -531,7 +534,12 @@ export const inferReactorFor = (policy: Partial<InferPolicy>, render: Render): R
|
|
|
531
534
|
})
|
|
532
535
|
])
|
|
533
536
|
const action = yield* (yield* Infer)
|
|
534
|
-
.react({
|
|
537
|
+
.react({
|
|
538
|
+
trajectory: input.trajectory,
|
|
539
|
+
identity: { ...self, turn: input.turn },
|
|
540
|
+
...(input.model === undefined ? {} : { model: input.model }),
|
|
541
|
+
...input.render
|
|
542
|
+
}, input.attempt)
|
|
535
543
|
.pipe(
|
|
536
544
|
Effect.catchCause((cause) =>
|
|
537
545
|
Cause.hasInterruptsOnly(cause)
|
package/src/cli/commands.ts
CHANGED
|
@@ -415,7 +415,8 @@ export const initCommand = Command.make("init", {
|
|
|
415
415
|
const initialized = yield* Effect.tryPromise({
|
|
416
416
|
try: () => initActor(name, {
|
|
417
417
|
cwd: cli.cwd,
|
|
418
|
-
...(directory === undefined ? {} : { directory })
|
|
418
|
+
...(directory === undefined ? {} : { directory }),
|
|
419
|
+
modelProtocol: answers.protocol
|
|
419
420
|
}),
|
|
420
421
|
catch: userErrorOf
|
|
421
422
|
})
|
package/src/cli/init.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { mkdir, rm, writeFile } from "node:fs/promises"
|
|
2
2
|
import { relative, resolve } from "node:path"
|
|
3
3
|
import { DEFAULT_PROJECT_CONFIG_PATH } from "tardie/server/config"
|
|
4
|
+
import type { ModelProtocol } from "tardie/model/directory"
|
|
4
5
|
|
|
5
6
|
import { CELLD_PROJECT_CONFIG_PATH, celldConfigOf } from "./celld"
|
|
6
7
|
import { actorTemplate } from "./template"
|
|
@@ -18,6 +19,7 @@ export interface InitActorOptions {
|
|
|
18
19
|
readonly directory?: string
|
|
19
20
|
readonly now?: Date
|
|
20
21
|
readonly packageVersion?: string
|
|
22
|
+
readonly modelProtocol?: ModelProtocol
|
|
21
23
|
}
|
|
22
24
|
|
|
23
25
|
export interface InitializedActor {
|
|
@@ -57,12 +59,31 @@ const manifestTemplate = (name: string, now: Date): string => `${JSON.stringify(
|
|
|
57
59
|
}
|
|
58
60
|
}, undefined, 2)}\n`
|
|
59
61
|
|
|
60
|
-
const
|
|
62
|
+
const adapterFor = (protocol: ModelProtocol): { readonly name: string; readonly source: string } => {
|
|
63
|
+
switch (protocol) {
|
|
64
|
+
case "anthropic-messages":
|
|
65
|
+
return { name: "anthropicAdapter", source: "tardie/model/anthropic" }
|
|
66
|
+
case "bedrock-converse":
|
|
67
|
+
return { name: "bedrockAdapter", source: "tardie/model/bedrock" }
|
|
68
|
+
case "openai-responses":
|
|
69
|
+
case "openai-chat-completions":
|
|
70
|
+
return { name: "openAICompatibleAdapter", source: "tardie/model/openai" }
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const workerTemplate = (protocol: ModelProtocol): string => {
|
|
75
|
+
const adapter = adapterFor(protocol)
|
|
76
|
+
return `import definition from "./actor"
|
|
61
77
|
import { ActorHost, cloudflareWorker } from "tardie/cloudflare"
|
|
78
|
+
import { modelAdapters } from "tardie/model/adapter"
|
|
79
|
+
import { ${adapter.name} } from "${adapter.source}"
|
|
62
80
|
|
|
63
81
|
export { ActorHost }
|
|
64
|
-
export default cloudflareWorker(definition
|
|
82
|
+
export default cloudflareWorker(definition, {
|
|
83
|
+
modelAdapters: modelAdapters(${adapter.name})
|
|
84
|
+
})
|
|
65
85
|
`
|
|
86
|
+
}
|
|
66
87
|
|
|
67
88
|
const packageTemplate = (version: string): string => `${JSON.stringify({
|
|
68
89
|
private: true,
|
|
@@ -88,7 +109,7 @@ export const initActor = async (name: string, options: InitActorOptions): Promis
|
|
|
88
109
|
|
|
89
110
|
try {
|
|
90
111
|
await writeFile(entry, source, "utf8")
|
|
91
|
-
await writeFile(worker, workerTemplate, "utf8")
|
|
112
|
+
await writeFile(worker, workerTemplate(options.modelProtocol ?? "openai-chat-completions"), "utf8")
|
|
92
113
|
await writeFile(manifest, manifestSource, "utf8")
|
|
93
114
|
await writeFile(celldManifest, celldConfigOf(manifestSource, manifest).source, "utf8")
|
|
94
115
|
await writeFile(packageManifest, packageTemplate(packageVersion), "utf8")
|
package/src/cloudflare/worker.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { DurableObject } from "cloudflare:workers"
|
|
2
2
|
import { Clock, Context, Effect, Layer, Schema } from "effect"
|
|
3
3
|
import { FetchHttpClient, HttpClient, HttpEffect, HttpRouter, HttpServerRequest, HttpServerResponse } from "effect/unstable/http"
|
|
4
|
-
import { actor, agentMethods, agentsPackage, applyModelPolicy, budget, codeMode, compaction, fetchPackage, Infer, infer as inferAgent, intersectModelPolicies, modelAllowedBy, outputValidateOnce, workspacePackage, type Actor, type ActorMethods, type ModelPolicy, type ModelRef } from "tardie"
|
|
4
|
+
import { actor, agentMethods, agentsPackage, applyModelPolicy, budget, codeMode, compaction, fetchPackage, Infer, infer as inferAgent, intersectModelPolicies, modelAllowedBy, outputValidateOnce, workspacePackage, type Actor, type ActorMethods, type InferenceObserver, type ModelPolicy, type ModelRef } from "tardie"
|
|
5
5
|
import type { Action } from "tardie/log/events"
|
|
6
6
|
import {
|
|
7
7
|
CATALOG_AVAILABILITY_FILTERS,
|
|
@@ -10,6 +10,10 @@ import {
|
|
|
10
10
|
MODEL_CATALOG_UNPRICED_ORDERS
|
|
11
11
|
} from "tardie/client/contract"
|
|
12
12
|
import { infer } from "tardie/model/model"
|
|
13
|
+
import {
|
|
14
|
+
modelAdapters,
|
|
15
|
+
type ModelAdapterRegistry
|
|
16
|
+
} from "tardie/model/adapter"
|
|
13
17
|
import { DEFAULT_MODEL_CATALOG_URL } from "tardie/model/metadata"
|
|
14
18
|
import {
|
|
15
19
|
loadModelCatalog,
|
|
@@ -79,6 +83,8 @@ interface MountedActor {
|
|
|
79
83
|
readonly name: string
|
|
80
84
|
readonly actor: DefaultAssembly
|
|
81
85
|
readonly methods: ActorMethods
|
|
86
|
+
readonly modelAdapters: ModelAdapterRegistry
|
|
87
|
+
readonly inferenceObserverFor?: (context: CloudflareWorkerLayerContext<Env>) => InferenceObserver
|
|
82
88
|
readonly layersFor?: (context: CloudflareWorkerLayerContext<Env>) => CloudflareLaneEnv<never>
|
|
83
89
|
}
|
|
84
90
|
|
|
@@ -182,7 +188,12 @@ const selectedModelFrom = (
|
|
|
182
188
|
return { reference: selected, provider, metadata: model.metadata, contextWindowTokens, catalogRevision: catalog.snapshot.revision }
|
|
183
189
|
}
|
|
184
190
|
|
|
185
|
-
const modelLayer = (
|
|
191
|
+
const modelLayer = (
|
|
192
|
+
models: CloudflareModels | undefined,
|
|
193
|
+
catalog: ModelCatalogState,
|
|
194
|
+
adapters: ModelAdapterRegistry,
|
|
195
|
+
observer?: InferenceObserver
|
|
196
|
+
) => {
|
|
186
197
|
if (models === undefined) {
|
|
187
198
|
const failed: Action = { kind: "fail", error: "no model is configured", failure: { cause: "inference_error", attempts: 1 } }
|
|
188
199
|
return Layer.succeed(Infer)({
|
|
@@ -226,7 +237,7 @@ const modelLayer = (models: CloudflareModels | undefined, catalog: ModelCatalogS
|
|
|
226
237
|
contextWindowTokens: selectedModel.contextWindowTokens,
|
|
227
238
|
...(selectedModel.metadata.maxOutputTokens === undefined ? {} : { maxOutputTokens: selectedModel.metadata.maxOutputTokens }),
|
|
228
239
|
...(selectedModel.metadata.pricing === undefined ? {} : { pricing: selectedModel.metadata.pricing })
|
|
229
|
-
})
|
|
240
|
+
}, adapters, observer === undefined ? {} : { observer })
|
|
230
241
|
return Effect.flatMap(Infer, (model) => model.react(request, key)).pipe(Effect.provide(selected))
|
|
231
242
|
}
|
|
232
243
|
})
|
|
@@ -396,6 +407,8 @@ export class ActorHost extends DurableObject<Env> {
|
|
|
396
407
|
|
|
397
408
|
private async openHost(): Promise<CloudflareHost> {
|
|
398
409
|
const models = modelsFrom(this.env)
|
|
410
|
+
const adapters = mountedActor?.modelAdapters ?? modelAdapters()
|
|
411
|
+
for (const provider of Object.values(models?.providers ?? {})) adapters.resolve(provider.protocol)
|
|
399
412
|
const catalog: ModelCatalogState = models === undefined
|
|
400
413
|
? { refreshError: "no model is configured" }
|
|
401
414
|
: await this.catalog()
|
|
@@ -462,7 +475,8 @@ export class ActorHost extends DurableObject<Env> {
|
|
|
462
475
|
principal,
|
|
463
476
|
actorFor: (lane) => threadOf(lane) === undefined ? undefined : selectedAssembly,
|
|
464
477
|
layersFor: (lane) => {
|
|
465
|
-
const
|
|
478
|
+
const observer = mountedActor?.inferenceObserverFor?.({ env: this.env, lane })
|
|
479
|
+
const framework = Layer.mergeAll(modelLayer(models, catalog, adapters, observer), FetchHttpClient.layer, sandboxLayer)
|
|
466
480
|
const application = mountedActor?.layersFor?.({ env: this.env, lane })
|
|
467
481
|
return application === undefined ? framework : Layer.mergeAll(framework, application)
|
|
468
482
|
},
|
|
@@ -846,8 +860,16 @@ type CloudflareWorkerLayersFor<R, WorkerEnv extends Env> = (
|
|
|
846
860
|
// CloudflareWorkerOptions supplies every actor requirement the Worker does not bind itself.
|
|
847
861
|
export type CloudflareWorkerOptions<R, WorkerEnv extends Env = Env> =
|
|
848
862
|
[CloudflareApplicationRequirements<R>] extends [never]
|
|
849
|
-
? {
|
|
850
|
-
|
|
863
|
+
? {
|
|
864
|
+
readonly layersFor?: CloudflareWorkerLayersFor<R, WorkerEnv>
|
|
865
|
+
readonly modelAdapters?: ModelAdapterRegistry
|
|
866
|
+
readonly inferenceObserverFor?: (context: CloudflareWorkerLayerContext<WorkerEnv>) => InferenceObserver
|
|
867
|
+
}
|
|
868
|
+
: {
|
|
869
|
+
readonly layersFor: CloudflareWorkerLayersFor<R, WorkerEnv>
|
|
870
|
+
readonly modelAdapters?: ModelAdapterRegistry
|
|
871
|
+
readonly inferenceObserverFor?: (context: CloudflareWorkerLayerContext<WorkerEnv>) => InferenceObserver
|
|
872
|
+
}
|
|
851
873
|
|
|
852
874
|
type CloudflareWorkerArguments<R, WorkerEnv extends Env> =
|
|
853
875
|
[CloudflareApplicationRequirements<R>] extends [never]
|
|
@@ -867,6 +889,10 @@ export const cloudflareWorker = <
|
|
|
867
889
|
name: definition.name,
|
|
868
890
|
actor: definition as unknown as DefaultAssembly,
|
|
869
891
|
methods: definition.methods,
|
|
892
|
+
modelAdapters: options?.modelAdapters ?? modelAdapters(),
|
|
893
|
+
...(options?.inferenceObserverFor === undefined ? {} : {
|
|
894
|
+
inferenceObserverFor: options.inferenceObserverFor as unknown as (context: CloudflareWorkerLayerContext<Env>) => InferenceObserver
|
|
895
|
+
}),
|
|
870
896
|
...(options?.layersFor === undefined ? {} : {
|
|
871
897
|
layersFor: options.layersFor as unknown as (context: CloudflareWorkerLayerContext<Env>) => CloudflareLaneEnv<never>
|
|
872
898
|
})
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import type { ModelMessage, StreamChunk, Tool } from "@tanstack/ai"
|
|
2
|
+
import type { ModelRequest } from "tardie/inference/request"
|
|
3
|
+
import type { OutputMode } from "tardie/output/contract"
|
|
4
|
+
import type { ModelPricing } from "tardie/inference/usage"
|
|
5
|
+
import type { ModelProtocol } from "./directory"
|
|
6
|
+
import type { OutputCapability } from "./output"
|
|
7
|
+
|
|
8
|
+
export interface StreamBounds {
|
|
9
|
+
readonly firstChunkMs: number
|
|
10
|
+
readonly idleMs: number
|
|
11
|
+
readonly totalMs: number
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export type ModelFetch = (
|
|
15
|
+
input: Parameters<typeof globalThis.fetch>[0],
|
|
16
|
+
init?: Parameters<typeof globalThis.fetch>[1]
|
|
17
|
+
) => Promise<Response>
|
|
18
|
+
|
|
19
|
+
export interface ModelConfig {
|
|
20
|
+
readonly baseUrl: string
|
|
21
|
+
readonly apiKey: string
|
|
22
|
+
readonly model: string
|
|
23
|
+
readonly protocol: ModelProtocol
|
|
24
|
+
readonly provider: string
|
|
25
|
+
// region selects the AWS region for a Bedrock Converse connection.
|
|
26
|
+
readonly region?: string
|
|
27
|
+
readonly contextWindowTokens: number
|
|
28
|
+
// maxOutputTokens caps every truncation-ladder rung; an omitted value uses the exported ladder.
|
|
29
|
+
readonly maxOutputTokens?: number
|
|
30
|
+
// maxTokensLadder replaces the exported truncation ladder before maxOutputTokens bounds it.
|
|
31
|
+
readonly maxTokensLadder?: ReadonlyArray<number>
|
|
32
|
+
// stream replaces any stated fields in DEFAULT_STREAM_BOUNDS.
|
|
33
|
+
readonly stream?: Partial<StreamBounds>
|
|
34
|
+
// output states the endpoint guarantee; an omitted value promises no native contract support.
|
|
35
|
+
readonly output?: OutputCapability
|
|
36
|
+
// pricing supplies the estimate used when the provider reports tokens without a billed cost.
|
|
37
|
+
readonly pricing?: ModelPricing
|
|
38
|
+
// throttleRetryDelaysMs sets the backoff bases and its length sets the retry count.
|
|
39
|
+
readonly throttleRetryDelaysMs?: ReadonlyArray<number>
|
|
40
|
+
// retryAfterJitterMs adds a random wait to a provider Retry-After value.
|
|
41
|
+
readonly retryAfterJitterMs?: number
|
|
42
|
+
// fetch replaces the transport for an embedding or test.
|
|
43
|
+
readonly fetch?: ModelFetch
|
|
44
|
+
// sleep replaces the retry wait for an embedding or test.
|
|
45
|
+
readonly sleep?: (ms: number) => Promise<void>
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export type ModelStopClass = "refused" | "truncated" | "violation" | "ok"
|
|
49
|
+
|
|
50
|
+
export interface ModelAdapterContext {
|
|
51
|
+
readonly config: ModelConfig
|
|
52
|
+
readonly request: ModelRequest
|
|
53
|
+
readonly mode: OutputMode
|
|
54
|
+
readonly maxTokens: number
|
|
55
|
+
readonly bounds: StreamBounds
|
|
56
|
+
readonly fetch: ModelFetch
|
|
57
|
+
readonly messages: ReadonlyArray<ModelMessage>
|
|
58
|
+
readonly tools: ReadonlyArray<Tool>
|
|
59
|
+
readonly systemPrompts: ReadonlyArray<string>
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export interface ModelAdapterAttempt {
|
|
63
|
+
readonly stream: AsyncIterable<StreamChunk>
|
|
64
|
+
readonly reportedUsage?: () => unknown
|
|
65
|
+
readonly stopClass?: () => ModelStopClass
|
|
66
|
+
readonly finishReason?: () => string | undefined
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export interface ModelAdapter {
|
|
70
|
+
readonly id: string
|
|
71
|
+
// ModelAdapter protocols select wire implementations independently of provider and model identity (adapter.test.ts, "resolves each protocol to its registered implementation").
|
|
72
|
+
readonly protocols: ReadonlyArray<ModelProtocol>
|
|
73
|
+
readonly start: (context: ModelAdapterContext) => ModelAdapterAttempt
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export interface ModelAdapterRegistry {
|
|
77
|
+
readonly protocols: ReadonlyArray<ModelProtocol>
|
|
78
|
+
readonly resolve: (protocol: ModelProtocol) => ModelAdapter
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// modelAdapters constructs an immutable protocol registry and rejects ambiguous implementations.
|
|
82
|
+
export const modelAdapters = (...adapters: ReadonlyArray<ModelAdapter>): ModelAdapterRegistry => {
|
|
83
|
+
const byProtocol = new Map<ModelProtocol, ModelAdapter>()
|
|
84
|
+
for (const adapter of adapters) {
|
|
85
|
+
for (const protocol of adapter.protocols) {
|
|
86
|
+
const previous = byProtocol.get(protocol)
|
|
87
|
+
if (previous !== undefined) {
|
|
88
|
+
throw new Error(`model protocol ${JSON.stringify(protocol)} has adapters ${JSON.stringify(previous.id)} and ${JSON.stringify(adapter.id)}`)
|
|
89
|
+
}
|
|
90
|
+
byProtocol.set(protocol, adapter)
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
const protocols = Object.freeze([...byProtocol.keys()])
|
|
94
|
+
return Object.freeze({
|
|
95
|
+
protocols,
|
|
96
|
+
resolve: (protocol: ModelProtocol): ModelAdapter => {
|
|
97
|
+
const adapter = byProtocol.get(protocol)
|
|
98
|
+
if (adapter !== undefined) return adapter
|
|
99
|
+
throw new Error(
|
|
100
|
+
`model protocol ${JSON.stringify(protocol)} has no registered adapter; register one with modelAdapters(...) before starting the host`
|
|
101
|
+
)
|
|
102
|
+
}
|
|
103
|
+
})
|
|
104
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { createAnthropicChat } from "@tanstack/ai-anthropic"
|
|
2
|
+
import { outputSchemaFor } from "./output"
|
|
3
|
+
import type { ModelAdapter } from "./adapter"
|
|
4
|
+
|
|
5
|
+
// anthropicAdapter binds the Anthropic Messages protocol through TanStack AI.
|
|
6
|
+
export const anthropicAdapter: ModelAdapter = {
|
|
7
|
+
id: "tanstack/anthropic",
|
|
8
|
+
protocols: ["anthropic-messages"],
|
|
9
|
+
start: ({ config, request, mode, fetch, messages, tools, systemPrompts }) => {
|
|
10
|
+
const outputSchema = request.output?.kind === "contract" && mode.kind === "native"
|
|
11
|
+
? outputSchemaFor(request.output, mode)
|
|
12
|
+
: undefined
|
|
13
|
+
const adapter = createAnthropicChat(config.model as never, config.apiKey, {
|
|
14
|
+
baseURL: config.baseUrl,
|
|
15
|
+
maxRetries: 0,
|
|
16
|
+
fetch
|
|
17
|
+
})
|
|
18
|
+
return {
|
|
19
|
+
stream: adapter.chatStream({
|
|
20
|
+
model: config.model,
|
|
21
|
+
messages: messages as never,
|
|
22
|
+
tools: tools as never,
|
|
23
|
+
systemPrompts,
|
|
24
|
+
...(outputSchema === undefined ? {} : { outputSchema }),
|
|
25
|
+
logger: new Proxy({}, { get: () => () => {} }) as never
|
|
26
|
+
} as never)
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
}
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
import * as BedrockRuntime from "@aws-sdk/client-bedrock-runtime"
|
|
2
|
+
import { FetchHttpHandler } from "@smithy/fetch-http-handler"
|
|
3
|
+
import { BedrockConverseTextAdapter, type BEDROCK_CONVERSE_MODELS } from "@tanstack/ai-bedrock"
|
|
4
|
+
import type { OutputRequest } from "tardie/inference/request"
|
|
5
|
+
import { NATIVE_MODE, type OutputMode } from "tardie/output/contract"
|
|
6
|
+
import { outputNameFor, outputSchemaFor } from "./output"
|
|
7
|
+
import type { ModelAdapter, ModelConfig, ModelStopClass, StreamBounds } from "./adapter"
|
|
8
|
+
|
|
9
|
+
type SmithyHandler = Pick<FetchHttpHandler, "handle" | "destroy">
|
|
10
|
+
|
|
11
|
+
const bedrockHandler = (config: ModelConfig, bounds: StreamBounds): SmithyHandler => {
|
|
12
|
+
const transport: Promise<SmithyHandler> =
|
|
13
|
+
(globalThis as { Bun?: unknown }).Bun === undefined
|
|
14
|
+
? Promise.resolve(new FetchHttpHandler({ requestTimeout: bounds.totalMs }))
|
|
15
|
+
: (() => {
|
|
16
|
+
const moduleName = "@smithy/node-http-handler"
|
|
17
|
+
return (import(/* @vite-ignore */ moduleName) as Promise<typeof import("@smithy/node-http-handler")>).then(
|
|
18
|
+
({ NodeHttpHandler: Handler }) =>
|
|
19
|
+
new Handler({
|
|
20
|
+
connectionTimeout: bounds.firstChunkMs,
|
|
21
|
+
socketTimeout: bounds.idleMs,
|
|
22
|
+
requestTimeout: bounds.totalMs,
|
|
23
|
+
throwOnRequestTimeout: true
|
|
24
|
+
})
|
|
25
|
+
)
|
|
26
|
+
})()
|
|
27
|
+
|
|
28
|
+
return {
|
|
29
|
+
handle: async (request, handlerOptions) => {
|
|
30
|
+
request.headers = Object.fromEntries(
|
|
31
|
+
Object.entries(request.headers).filter(([key]) => key.toLowerCase() !== "authorization")
|
|
32
|
+
)
|
|
33
|
+
request.headers["cf-aig-authorization"] = `Bearer ${config.apiKey}`
|
|
34
|
+
return (await transport).handle(request, handlerOptions)
|
|
35
|
+
},
|
|
36
|
+
destroy: () => {
|
|
37
|
+
void transport.then((handler) => handler.destroy()).catch(() => undefined)
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export const converseOutputConfig = (
|
|
43
|
+
output: OutputRequest,
|
|
44
|
+
mode: OutputMode
|
|
45
|
+
): BedrockRuntime.OutputConfig | undefined => {
|
|
46
|
+
const schema = outputSchemaFor(output, mode)
|
|
47
|
+
const name = outputNameFor(output, mode)
|
|
48
|
+
if (schema === undefined || name === undefined) return undefined
|
|
49
|
+
return { textFormat: { type: "json_schema", structure: { jsonSchema: { name, schema: JSON.stringify(schema) } } } }
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export const converseStopClass = (stopReason: string | undefined): ModelStopClass => {
|
|
53
|
+
switch (stopReason) {
|
|
54
|
+
case "guardrail_intervened":
|
|
55
|
+
case "content_filtered":
|
|
56
|
+
return "refused"
|
|
57
|
+
case "max_tokens":
|
|
58
|
+
case "model_context_window_exceeded":
|
|
59
|
+
return "truncated"
|
|
60
|
+
case "malformed_model_output":
|
|
61
|
+
return "violation"
|
|
62
|
+
default:
|
|
63
|
+
return "ok"
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export const tapStopReason = <T>(
|
|
68
|
+
stream: AsyncIterable<T>,
|
|
69
|
+
into: { stopReason?: string }
|
|
70
|
+
): AsyncIterable<T> => ({
|
|
71
|
+
async *[Symbol.asyncIterator]() {
|
|
72
|
+
for await (const event of stream) {
|
|
73
|
+
const stop = (event as { messageStop?: { stopReason?: unknown } }).messageStop?.stopReason
|
|
74
|
+
if (typeof stop === "string") into.stopReason = stop
|
|
75
|
+
yield event
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
export const tapConverseUsage = <T>(
|
|
81
|
+
stream: AsyncIterable<T>,
|
|
82
|
+
into: { usage?: unknown }
|
|
83
|
+
): AsyncIterable<T> => ({
|
|
84
|
+
async *[Symbol.asyncIterator]() {
|
|
85
|
+
for await (const event of stream) {
|
|
86
|
+
const usage = (event as { metadata?: { usage?: unknown } }).metadata?.usage
|
|
87
|
+
if (usage !== undefined) into.usage = usage
|
|
88
|
+
yield event
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
})
|
|
92
|
+
|
|
93
|
+
export const bedrockConverseTextAdapter = (
|
|
94
|
+
config: ModelConfig,
|
|
95
|
+
maxTokens: number,
|
|
96
|
+
bounds: StreamBounds,
|
|
97
|
+
output?: OutputRequest,
|
|
98
|
+
mode: OutputMode = NATIVE_MODE,
|
|
99
|
+
stops: { stopReason?: string } = {},
|
|
100
|
+
reported: { usage?: unknown } = {}
|
|
101
|
+
) => {
|
|
102
|
+
const handler = bedrockHandler(config, bounds)
|
|
103
|
+
const region = config.region ?? config.baseUrl.split("/").filter((s) => s !== "").at(-1)
|
|
104
|
+
if (region === undefined) throw new Error("a Bedrock connection must declare its AWS region")
|
|
105
|
+
return new (class extends BedrockConverseTextAdapter<(typeof BEDROCK_CONVERSE_MODELS)[number]> {
|
|
106
|
+
protected override importBedrockRuntime(): Promise<typeof BedrockRuntime> {
|
|
107
|
+
return Promise.resolve(BedrockRuntime)
|
|
108
|
+
}
|
|
109
|
+
protected override buildClientConfig(
|
|
110
|
+
resolved: Parameters<BedrockConverseTextAdapter<(typeof BEDROCK_CONVERSE_MODELS)[number]>["buildClientConfig"]>[0],
|
|
111
|
+
resolvedRegion: string,
|
|
112
|
+
endpoint: string | undefined
|
|
113
|
+
) {
|
|
114
|
+
return { ...super.buildClientConfig(resolved, resolvedRegion, endpoint), requestHandler: handler, maxAttempts: 1 }
|
|
115
|
+
}
|
|
116
|
+
public override buildInput(options: Parameters<BedrockConverseTextAdapter<(typeof BEDROCK_CONVERSE_MODELS)[number]>["buildInput"]>[0]) {
|
|
117
|
+
const input = super.buildInput(options) as BedrockRuntime.ConverseStreamCommandInput
|
|
118
|
+
input.inferenceConfig = { ...input.inferenceConfig, maxTokens }
|
|
119
|
+
const outputConfig = output === undefined ? undefined : converseOutputConfig(output, mode)
|
|
120
|
+
if (outputConfig !== undefined) input.outputConfig = { ...input.outputConfig, ...outputConfig }
|
|
121
|
+
return input
|
|
122
|
+
}
|
|
123
|
+
protected override async sendStream(input: BedrockRuntime.ConverseStreamCommandInput) {
|
|
124
|
+
const stream = await super.sendStream(input)
|
|
125
|
+
return tapStopReason(tapConverseUsage(stream, reported), stops)
|
|
126
|
+
}
|
|
127
|
+
})({ apiKey: "byok", region, baseURL: config.baseUrl }, config.model as (typeof BEDROCK_CONVERSE_MODELS)[number])
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// bedrockAdapter binds the Bedrock Converse protocol through the AWS and TanStack adapters.
|
|
131
|
+
export const bedrockAdapter: ModelAdapter = {
|
|
132
|
+
id: "tanstack/bedrock-converse",
|
|
133
|
+
protocols: ["bedrock-converse"],
|
|
134
|
+
start: ({ config, request, mode, maxTokens, bounds, messages, tools, systemPrompts }) => {
|
|
135
|
+
const stops: { stopReason?: string } = {}
|
|
136
|
+
const reported: { usage?: unknown } = {}
|
|
137
|
+
const adapter = bedrockConverseTextAdapter(config, maxTokens, bounds, request.output, mode, stops, reported)
|
|
138
|
+
return {
|
|
139
|
+
stream: adapter.chatStream({
|
|
140
|
+
model: config.model,
|
|
141
|
+
messages: messages as never,
|
|
142
|
+
tools: tools as never,
|
|
143
|
+
systemPrompts,
|
|
144
|
+
modelOptions: { max_tokens: maxTokens } as never,
|
|
145
|
+
logger: new Proxy({}, { get: () => () => {} }) as never
|
|
146
|
+
} as never),
|
|
147
|
+
reportedUsage: () => reported.usage,
|
|
148
|
+
stopClass: () => converseStopClass(stops.stopReason),
|
|
149
|
+
finishReason: () => stops.stopReason
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// bedrockAdapterForBun verifies the Bun transport dependency before the host accepts Bedrock configuration.
|
|
155
|
+
export const bedrockAdapterForBun = async (): Promise<ModelAdapter> => {
|
|
156
|
+
await import("@smithy/node-http-handler")
|
|
157
|
+
return bedrockAdapter
|
|
158
|
+
}
|