diff --git a/AGENTS.md b/AGENTS.md index 043130a..29ab4ac 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -68,6 +68,12 @@ Spec drift is never "someone else's problem to fix later." It compounds. --- +## Rule 7: Place helper functions at the end of implementation files + +When adding or refactoring helper functions in source files, place them after the main class/function implementations (i.e. helper functions last in the file), unless language constraints require otherwise. + +--- + ## Verification Checklist Run this checklist after every non-trivial change: diff --git a/packages/core/src/agent-session-do.ts b/packages/core/src/agent-session-do.ts index 7c1bf7d..dbc06c4 100644 --- a/packages/core/src/agent-session-do.ts +++ b/packages/core/src/agent-session-do.ts @@ -43,6 +43,8 @@ import type { } from "@piccolo/api"; import type { FinishReason, LanguageModel, LanguageModelUsage, ModelMessage } from "ai"; import { stepCountIs, streamText } from "ai"; +import { createAiGateway } from "ai-gateway-provider"; +import { createUnified } from "ai-gateway-provider/providers/unified"; import { agentCompact, splitForCompaction } from "./agent-compact.ts"; import { toAiSdkTools } from "./agent-tools.ts"; import type { @@ -57,7 +59,6 @@ import type { import { generateEntryId, isLegacySystemMessageData, parseEntry } from "./db/entry-types.ts"; import { getEntries, getSession } from "./db/schema.ts"; import { ExtensionRunner } from "./extension-runner.ts"; -import { createModel } from "./gateway.ts"; import { Messages } from "./messages.ts"; import { ObservableImpl } from "./observable-impl.ts"; import { buildSessionContext, walkToRoot } from "./session/context.ts"; @@ -1196,3 +1197,12 @@ function estimateTokens(messages: ModelMessage[]): number { } return Math.ceil(chars / 4); } + +function createModel(env: Env, modelId: string): LanguageModel { + const gateway = createAiGateway({ + accountId: env.CF_ACCOUNT_ID, + gateway: env.CF_AI_GATEWAY_NAME, + apiKey: env.CF_AI_GATEWAY_TOKEN, + }); + return gateway(createUnified()(modelId)) as LanguageModel; +} diff --git a/packages/core/src/gateway.ts b/packages/core/src/gateway.ts deleted file mode 100644 index 9eca046..0000000 --- a/packages/core/src/gateway.ts +++ /dev/null @@ -1,41 +0,0 @@ -/** - * AI Gateway / model construction for piccolo-core. - * - * `createModel(env, modelId)` is the single factory used by `AgentSessionDO` - * to create a `LanguageModel` for the agent. All LLM calls are routed through - * the Cloudflare AI Gateway unified endpoint. - * - * Model IDs use the "{provider}/{model-id}" format, e.g. - * "anthropic/claude-sonnet-4-5" - * "openai/gpt-4o" - * - * Spec refs: - * specs/agent.md §LLM Backend: Cloudflare AI Gateway - * specs/core.md §Bindings (CF_ACCOUNT_ID, CF_AI_GATEWAY_NAME, CF_AI_GATEWAY_TOKEN) - */ - -import type { LanguageModel } from "ai"; -import { createAiGateway } from "ai-gateway-provider"; -import { createUnified } from "ai-gateway-provider/providers/unified"; - -/** - * Construct a LanguageModel routed through the Cloudflare AI Gateway. - * - * Reads gateway credentials from the Worker env: - * env.CF_ACCOUNT_ID — Cloudflare account ID (var) - * env.CF_AI_GATEWAY_NAME — AI Gateway name / slug (var) - * env.CF_AI_GATEWAY_TOKEN — AI Gateway API token (secret) - * - * @param env Worker environment bindings - * @param modelId "{provider}/{model-id}" string, e.g. "anthropic/claude-sonnet-4-5" - */ -export function createModel(env: Env, modelId: string): LanguageModel { - const gateway = createAiGateway({ - accountId: env.CF_ACCOUNT_ID, - gateway: env.CF_AI_GATEWAY_NAME, - apiKey: env.CF_AI_GATEWAY_TOKEN, - }); - // createUnified() returns an OpenAI-compatible provider that accepts - // "{provider}/{model-id}" as the model string. - return gateway(createUnified()(modelId)) as LanguageModel; -} diff --git a/packages/core/test/do/mock-model.ts b/packages/core/test/do/mock-model.ts index ec37d26..6d82487 100644 --- a/packages/core/test/do/mock-model.ts +++ b/packages/core/test/do/mock-model.ts @@ -4,8 +4,8 @@ * Mirrors packages/agent/test/mock-gateway.ts but is kept separate so * packages/agent's test helpers do not need to be part of its public API. * - * The `AgentSessionDO` constructor calls `createModel(env, modelId)` from - * gateway.ts which uses ai-gateway-provider. Tests bypass the real gateway by + * The `AgentSessionDO` constructor calls an internal `createModel(env, modelId)` + * helper that uses ai-gateway-provider. Tests bypass the real gateway because * overriding `createModel` is not viable inside a Durable Object context. * * Instead, tests use `runInDurableObject` to reach directly into the DO diff --git a/specs/core.md b/specs/core.md index ea365a5..551c88d 100644 --- a/specs/core.md +++ b/specs/core.md @@ -377,7 +377,7 @@ async newSession( b. Derive `sessionId = this.ctx.id.name` (the DO was named with `idFromName(uuid)`). c. Set `state.sessionId`, `state.userId`, `state.modelId`, `state.name` where `state.name = options.name ?? sessionId`. d. Persist `sessionId`, `modelId`, and `name` to DO storage for cold-start recovery. - e. Reconstruct the `LanguageModel` via `createModel(env, modelId)`. + e. Reconstruct the `LanguageModel` via the internal `createModel(env, modelId)` helper in `agent-session-do.ts`. 2. Return `getSession(userId)` — the DO's own `SessionImpl` RpcTarget. The D1 `sessions` row is **not** written here — it is written lazily on the first persisted entry append (see §Lazy session creation). @@ -871,4 +871,4 @@ Uses `generateText` (non-streaming) with the same `LanguageModel` as the DO. Cal ### LLM Backend -All LLM calls go through the **Cloudflare AI Gateway** unified endpoint. `createModel(env, modelId)` in `gateway.ts` constructs the `LanguageModel` via `ai-gateway-provider`. Models are addressed as `{provider}/{model-id}` (e.g. `anthropic/claude-sonnet-4-5`). `piccolo-core` constructs the model; the `Agent` class has no knowledge of how it was built. +All LLM calls go through the **Cloudflare AI Gateway** unified endpoint. `createModel(env, modelId)` in `agent-session-do.ts` constructs the `LanguageModel` via `ai-gateway-provider`. Models are addressed as `{provider}/{model-id}` (e.g. `anthropic/claude-sonnet-4-5`). `piccolo-core` constructs the model; the `Agent` class has no knowledge of how it was built. diff --git a/specs/implementation_plan.md b/specs/implementation_plan.md index 87b27e8..ec593e5 100644 --- a/specs/implementation_plan.md +++ b/specs/implementation_plan.md @@ -265,7 +265,7 @@ The Durable Object that owns a live session. Wires `piccolo-agent` to persistenc | `packages/core/src/do/agent-session.ts` | `AgentSessionDO` class — full DO implementation | | `packages/core/src/do/compaction.ts` | `compact()` helper; creates `CompactionEntry`, calls `agentCompact()` | | `packages/core/src/do/retry.ts` | `checkRetry()` with exponential backoff, transient/overflow classification | -| `packages/core/src/do/gateway.ts` | `createModel()` — wraps `ai-gateway-provider` with env bindings | +| `packages/core/src/agent-session-do.ts` | Internal `createModel()` helper — wraps `ai-gateway-provider` with env bindings | | `packages/core/src/do/system-prompt.ts` | `buildBasePrompt(agentName)` — renders the base prompt template | | `packages/core/src/do/stubs.ts` | `ExtensionRunnerStub`, `SystemPromptAssemblerStub` — no-op stubs for steps 6–7 | | `packages/core/src/do/types-internal.ts` | `SystemPromptAddition`, `MODEL_CATALOG`, `resolveModel()` | @@ -514,7 +514,7 @@ The Worker that serves the Cap'n Web RPC endpoint and proxies to `piccolo-core`. ### 10.1 Implementation Notes -**Status:** Complete. 51 tests pass (383 total across all packages), coverage above thresholds. `pnpm biome check .` passes with 1 pre-existing `noNonNullAssertion` warning. `pnpm -r exec tsc --noEmit` has one pre-existing error in `packages/core/src/gateway.ts` (unrelated to step 10). +**Status:** Complete. 51 tests pass (383 total across all packages), coverage above thresholds. `pnpm biome check .` passes with 1 pre-existing `noNonNullAssertion` warning. `pnpm -r exec tsc --noEmit` had one historical pre-existing error in the former `packages/core/src/gateway.ts` (unrelated to step 10). **Post-review corrections (all changes fully spec-synced):** 1. `ITextUI.show*()` → `ITextUI.get*()` — direction fix: tool provides rendering text to gateway (pull), not gateway pushes to tool.