From b8d45dbcc22dcc8638580068569ae8a14a047c2d Mon Sep 17 00:00:00 2001 From: rgarcia <72655+rgarcia@users.noreply.github.com> Date: Tue, 18 Aug 2026 19:14:36 +0000 Subject: [PATCH] Delete superseded docs and dead code --- .github/workflows/ci.yml | 2 - docs/agent-tool-configuration-spec.md | 642 ------------------ docs/architecture.md | 6 +- package-lock.json | 74 ++ packages/loop/README.md | 13 +- packages/loop/examples/agent-openai-smoke.ts | 47 -- .../{agent-provider-matrix.ts => agent.ts} | 10 +- .../loop/examples/harness-openai-smoke.ts | 55 -- ...{harness-provider-matrix.ts => harness.ts} | 13 +- packages/loop/examples/shared/options.ts | 33 + packages/loop/package.json | 6 +- packages/loop/src/core/actions/computer.ts | 11 - packages/loop/src/core/actions/index.ts | 36 +- packages/loop/src/pi/index.ts | 2 - packages/loop/src/pi/providers.ts | 4 - packages/loop/src/pi/providers/common.ts | 28 - packages/loop/test/api-keys.test.ts | 5 - ...ix.test.ts => example-tool-policy.test.ts} | 28 +- 18 files changed, 156 insertions(+), 859 deletions(-) delete mode 100644 docs/agent-tool-configuration-spec.md delete mode 100644 packages/loop/examples/agent-openai-smoke.ts rename packages/loop/examples/{agent-provider-matrix.ts => agent.ts} (78%) delete mode 100644 packages/loop/examples/harness-openai-smoke.ts rename packages/loop/examples/{harness-provider-matrix.ts => harness.ts} (74%) create mode 100644 packages/loop/examples/shared/options.ts rename packages/loop/test/{example-provider-matrix.test.ts => example-tool-policy.test.ts} (59%) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index cad52ec1..7de571c1 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -98,7 +98,6 @@ jobs: OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }} GOOGLE_API_KEY: ${{ secrets.GOOGLE_API_KEY }} - META_API_KEY: ${{ secrets.META_API_KEY }} XAI_API_KEY: ${{ secrets.XAI_API_KEY }} MOONSHOT_API_KEY: ${{ secrets.MOONSHOT_API_KEY }} run: npm run test:integration --workspace @onkernel/loop @@ -123,7 +122,6 @@ jobs: OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }} GOOGLE_API_KEY: ${{ secrets.GOOGLE_API_KEY }} - META_API_KEY: ${{ secrets.META_API_KEY }} XAI_API_KEY: ${{ secrets.XAI_API_KEY }} MOONSHOT_API_KEY: ${{ secrets.MOONSHOT_API_KEY }} run: npm run test:integration --workspace @onkernel/loop -- test/e2e.live.test.ts diff --git a/docs/agent-tool-configuration-spec.md b/docs/agent-tool-configuration-spec.md deleted file mode 100644 index b8e9e34c..00000000 --- a/docs/agent-tool-configuration-spec.md +++ /dev/null @@ -1,642 +0,0 @@ -# Agent Tool Configuration - -**Status:** Superseded by `attach()` (2026-08-14). The tool-array-as-single-source-of-truth -rule it establishes still holds; what changed is where the array lives. `CuaAgent` and -`CuaAgentHarness` are gone, and a caller now compiles a (model, tools) pair through -`attach()` and owns the selection itself, so the `setTools()`/`setModel()` mutation surface -and its in-tool `executionMode: "sequential"` guard described below no longer exist. -Retained as the record of why the tool array is explicit and required. - -**Scope:** `@onkernel/loop` (written when this code lived in two packages) - -**Compatibility:** Not a goal; these packages are alpha and may make breaking API changes. - -## Summary - -`CuaAgent` and `CuaAgentHarness` should have one explicit source of truth for the tools exposed to a model: a required `tools` array. `tools: []` is valid for a text-only agent. - -The array may contain: - -- Loop-authored tools, such as browser snapshots, browser action plans, browser waits, batches, and `playwright_execute` -- provider-defined native browser or computer tools and predefined toolsets -- ordinary caller-provided `AgentTool` objects - -Provider namespaces expose only surfaces justified by linked first-party documentation. Loop-authored capabilities remain separate under `loop.tools` and `loop.toolsets`. A caller can combine either category with custom application tools while seeing exactly what the model receives. - -The former `extraTools`, `mode`, `nativeTool`, and `playwright` constructor options are removed. `activeToolNames`, `setActiveTools()`, `setMode()`, `getMode()`, `computer_use_extra`, and Loop-generated default system prompts are also removed. No global or derived mode replaces them. - -Each Loop tool specification must contain enough information to build, expose, execute, and describe that tool independently. Convenience toolsets may return arrays of tool specifications, but they must not establish hidden runtime state or add undeclared tools. - -## Motivation - -The current constructor spreads tool configuration across four arguments: - -```ts -new CuaAgent({ - extraTools, - mode, - nativeTool, - playwright, - // ... -}); -``` - -A caller cannot determine the final model-facing tool list by reading those fields independently: - -- `mode` selects a provider-dependent default action set and changes tool names. -- `nativeTool` replaces canonical tools and constrains the provider and mode. -- `playwright` adds another tool. -- `extraTools` appends caller tools. -- `computer_use_extra` is added implicitly. -- `CuaAgentHarness.activeToolNames` creates a second installed-versus-active configuration layer. - -The same configuration also controls unrelated runtime policy: coordinate interpretation, post-action screenshots, system instructions, provider eligibility, and payload rewriting. This makes new interaction methods difficult to add and makes the actual model-facing surface difficult to predict. - -The public distinction should instead be simple: - -- A **tool** is a callable capability directly exposed to the model. -- An **action** is an operation requested within a tool call. -- A **toolset** is an ordinary array of tools chosen by the caller. - -For example, Anthropic's native `browser` is one tool with multiple actions. `browser_act` is one tool containing a sequence of step actions. `browser_snapshot` is a single-purpose tool whose input does not need an action discriminator. - -`ComputerUseAction` may remain an internal normalized execution representation, but agent constructors should not expose it as their tool-selection API. - -This terminology must also be used consistently in the architecture document, package READMEs, API documentation, and user-facing examples. Users should not need to understand the internal action IR to configure model-facing tools. - -## Goals - -1. Make the exact model-facing tool catalog obvious at the constructor call site. -2. Support minimal and empty configurations without hidden additions. -3. Distinguish first-party provider surfaces from Loop-authored capabilities. -4. Allow provider-native, Loop-authored, Playwright, and caller tools to compose in one list. -5. Let every tool own the runtime policy required to execute it correctly. -6. Support cache-aware mid-conversation tool additions, removals, and replacements. -7. Validate tool/model and tool/tool incompatibilities directly and early. -8. Permit experimentation with new tools without adding constructor flags or global modes. - -## Non-goals - -- Preserving the current constructor API -- Preserving current mode-dependent tool aliases -- Automatically selecting a supposedly optimal toolset for a model -- Exposing the internal canonical action IR as constructor configuration -- Automatically adding prerequisite, navigation, screenshot, or fallback tools -- Silently replacing incompatible tools when the model changes -- Preserving Loop's current default system prompts - -## Public namespace - -Tool factories and toolsets should be exported through one discoverable namespace rather than as a collection of global functions. - -Loop-authored capabilities live under `loop.tools` and `loop.toolsets`: - -```ts -loop.tools.browser.snapshot() -loop.tools.browser.act() -loop.tools.browser.waitFor() -loop.tools.browser.batch(...) -loop.tools.computer.batch(...) -loop.tools.playwright() - -loop.toolsets.browser() -loop.toolsets.computer() -loop.toolsets.mixed() -``` - -Provider-defined surfaces live under provider namespaces and carry their first-party source: - -```ts -loop.providers.anthropic.source -loop.providers.anthropic.tools.browser(...) -loop.providers.anthropic.tools.computer(...) - -loop.providers.google.source -loop.providers.google.toolsets.browser() -``` - -The distinction is deliberate: - -- `loop.providers.` contains only documented native declarations or predefined toolsets. Each namespace exposes its first-party `source` or versioned `sources`, and every returned spec carries the applicable URL. -- `loop.tools` contains additional tools Loop designed, such as snapshots, semantic waits, browser action plans, browser batches, and Playwright execution. -- `loop.toolsets` contains Loop-curated combinations of Loop-authored tools. - -The exact property names may be refined, but the final exports must remain namespaced, autocomplete-friendly, and free of a large flat list of package-level tool factory functions. - -## Constructor API - -Both constructors accept one required top-level `tools` array: - -```ts -const agent = new CuaAgent({ - browser, - client, - initialState: { model: "anthropic:claude-opus-5" }, - tools: [ - loop.tools.browser.snapshot(), - loop.tools.browser.act(), - customerLookupTool, - ], -}); -``` - -```ts -const harness = new CuaAgentHarness({ - browser, - client, - session, - model: "openai:gpt-5.6-sol", - tools: [ - loop.tools.playwright(), - ], -}); -``` - -Conceptually: - -```ts -// Defined and exported by @onkernel/loop. -type LoopAgentTool = LoopToolSpec | AgentTool; - -interface CuaAgentOptions { - // Existing non-tool options omitted. - tools: LoopAgentTool[]; -} - -interface CuaAgentHarnessOptions { - // Existing non-tool options omitted. - tools: LoopAgentTool[]; -} -``` - -A `LoopToolSpec` is declarative because `@onkernel/loop` must materialize it against the Kernel browser, SDK client, selected model, and provider transport. An `AgentTool` is already executable and can be installed directly; the tool manager projects it into a fresh declaration-only object before the catalog is compiled, so compilation never sees executors. - -There is one current tool list, not separate installed and active lists. `setTools()` changes that list for subsequent provider requests. - -## Exact configurations - -### Native Anthropic browser plus an unrelated custom tool - -```ts -tools: [ - loop.providers.anthropic.tools.browser({ - version: "20260701", - javascript: true, - }), - customerLookupTool, -] -``` - -The model receives exactly the native browser tool and `customer_lookup`. Loop must not add canonical browser tools, navigation helpers, screenshots, batches, or Playwright. - -### Playwright only - -```ts -tools: [ - loop.tools.playwright(), -] -``` - -The model receives exactly `playwright_execute`. - -### Browser action plans only - -```ts -tools: [ - loop.tools.browser.act(), -] -``` - -The model receives exactly `browser_act`. Loop may warn that ref-based steps require refs from another source, but it must not silently add a snapshot tool. - -A practical minimal ref-based plan configuration is explicit: - -```ts -tools: [ - loop.tools.browser.snapshot(), - loop.tools.browser.act(), -] -``` - -### A caller-composed browser catalog - -```ts -tools: [ - loop.tools.browser.snapshot(), - loop.tools.browser.find(), - loop.tools.browser.text(), - loop.tools.browser.act(), - loop.tools.browser.waitFor(), - loop.tools.browser.navigate(), -] -``` - -### Native and Loop-authored tools together - -```ts -tools: [ - loop.providers.anthropic.tools.computer({ version: "20260701" }), - loop.tools.browser.snapshot(), - loop.tools.browser.act(), -] -``` - -This combination is valid only if the provider transport accepts the native declaration alongside ordinary function tools. Validation belongs to the selected tool specifications and provider request composer, not to a global mode check. - -### Text-only agent - -```ts -tools: [] -``` - -No tool is added implicitly. - -## Convenience toolsets - -Convenience helpers provide ordinary arrays: - -```ts -tools: loop.providers.google.toolsets.browser() -``` - -```ts -tools: loop.toolsets.browser() -``` - -```ts -tools: loop.toolsets.mixed() -``` - -Callers can inspect and compose them: - -```ts -tools: [ - ...loop.toolsets.browser(), - customerLookupTool, -] -``` - -A toolset has no runtime meaning after expansion. It does not set or imply a mode. The runtime receives only the resulting tool specifications. - -Every toolset must document and test its exact members. Tools such as -`browser_act` remain outside the reusable base toolset so applications opt into -them explicitly: - -```ts -tools: [ - ...loop.toolsets.browser(), - loop.tools.browser.act(), -] -``` - -The CLI uses this explicit composition for its structured Loop-browser catalogs. - -Provider toolsets must expose the first-party source they mirror and must not silently include Loop-authored additions. - -## No global or derived mode - -The runtime must not derive `computer`, `browser`, or `hybrid` state from the selected tools. Those labels are too coarse to govern execution safely. - -Instead, each `LoopToolSpec` supplies the policy needed for that tool to do its work: - -- stable tool identity and preferred model-facing name -- description and schema or native declaration -- declarative local-execution policy (action conversion, coordinate contract) -- provider and model compatibility checks -- request headers and payload transformation, when required -- incoming native-call normalization, when required -- coordinate contract and conversion, when applicable -- explicit result formatting -- conflicts with other tool specifications - -Examples: - -- A computer click tool owns its provider coordinate conversion and OS-level input execution. -- A browser click tool owns viewport/ref targeting and CDP execution. -- A native Anthropic browser tool owns its beta header, native declaration, input mapping, and first-failure rules. -- `playwright_execute` owns its execution context and does not imply screenshot or computer tools. -- `browser_act` owns semantic polling, plan deadlines, and stable successor collection. - -Screenshots are returned only when the model explicitly calls a screenshot or zoom action. Write actions do not capture an image automatically; semantic tools such as `browser_act` return their own structured successor feedback. - -Tools may share internal resources such as one CDP connection, ref lifecycle, or Kernel client. Resource sharing must be explicit runtime infrastructure and must not create a hidden mode or alter the caller's tool list. - -## Tool names and collisions - -A tool specification has a stable identity and a preferred model-facing name. The compiled catalog resolves its final model-facing name. - -Composition sees the complete requested list and must detect name collisions before the first request. It must never silently shadow a tool. - -The implemented naming policy is: - -1. Keep preferred declared names when unique. -2. Reject collisions by default with an error naming both tool identities. -3. Permit an explicit alias or namespace option when the underlying provider allows renaming. -4. Reject aliases for native tools whose server-defined name is fixed. -5. Never rename an existing tool as a side effect of adding another tool mid-conversation. - -A toolset factory should not need hidden global state. The central composer sees all expanded tool specs and applies the collision policy. A toolset may expose explicit naming or namespace options, but automatic context-sensitive aliasing must not make the resulting catalog unpredictable. - -Catalog tests cover provider-native tools composed with Loop browser and caller tools, and verify first-party sources for every provider surface. - -## Tools and actions - -Public documentation should use these terms consistently. - -### Tool - -A callable entry in the provider request's tool catalog. - -Examples: - -- `browser_act` -- `browser_snapshot` -- `computer_batch` -- `browser_batch` -- `playwright_execute` -- Anthropic's native `browser` -- a caller's `customer_lookup` - -### Action - -An operation selected through a tool's arguments. - -Current action-bearing tools include: - -- provider-native computer and browser tools, which use an `action` discriminator -- `computer_batch`, which accepts an ordered `actions` array -- `browser_batch`, which would accept ordered browser-plane actions -- `browser_act`, whose `steps` are dependent browser actions with optional semantic expectations - -Some single-purpose tools do not need an explicit action argument. Internally converting their call into a `ComputerUseAction` does not make the public callable surface an action. - -## Batch tools - -Batch tools need first-class treatment in this design rather than inheriting an unexplained default action set. - -### Computer batch - -`computer_batch` is a Loop-authored tool over computer-plane actions. Its factory should let the caller control the allowed action schema: - -```ts -loop.tools.computer.batch({ - actions: ["click", "type", "keypress", "screenshot"], -}) -``` - -A Loop toolset may choose and document a default batch configuration, but constructing the batch tool directly must make its allowed actions visible. The batch must not gain actions merely because unrelated individual tools are present. - -### Browser batch - -Loop offers a browser-plane equivalent that does not dispatch OS computer-use input: - -```ts -loop.tools.browser.batch({ - actions: ["snapshot", "click", "fill", "wait_for", "text"], -}) -``` - -The browser batch executes browser/CDP operations sequentially over one shared ref table and returns ordered read results. It short-circuits on the first failed or unsatisfied boundary and reports the failed index and skipped count. Images appear only for explicit screenshot steps. - -### Browser batch versus browser act - -`browser_batch` and `browser_act` must not become two vague names for the same feature: - -- `browser_batch` is a mechanical ordered container for explicitly selected browser actions and read results. -- `browser_act` is a dependent plan with per-step and final semantic expectations, causal outcomes, deadlines, stop reasons, and stable successor feedback. - -The implemented batch is intentionally not a restricted action-plan tool. Ref-producing reads update the shared ref table before later actions, but the input has no interpolation, saved-value, branch, or workflow syntax. `browser_act` remains the semantic planning surface. - -### Native action restrictions - -A server-defined native tool may not permit action restriction. Its factory must reject unsupported configuration rather than pretend to narrow the provider schema. - -## Tool composition - -Tool specifications are composed before a provider request. - -Composition must: - -1. Materialize each requested tool against the browser and client. -2. Resolve or reject model-facing name collisions. -3. Validate every tool against the selected model and provider. -4. Compose compatible headers and payload transforms. -5. Reject conflicting transforms with an error naming the conflicting tools. -6. Establish explicit resource sharing without adding tools. -7. Install exactly the requested tools. - -Payload transforms must operate on explicit tool identities, not infer ownership from names such as `click`. This is required for native-tool adapters that classify or replace tools by name. - -## Mid-conversation tool changes and provider caches - -Both agent classes must support changing the exact tool list between model requests: - -```ts -await harness.setTools([ - ...harness.getTools(), - loop.tools.browser.act(), -]); -``` - -A tool may also arrange for tools to be added during its own execution so they are available to the immediately following model request. - -The implementation should build on pi's dynamic tool-loading semantics: - -- Detect purely additive changes. -- Record newly available tool names at the tool-result position. -- Use Anthropic deferred tool definitions and tool references when the selected model supports them. -- Use OpenAI tool-search calls and outputs when the selected model supports them. -- Fall back to sending the complete current tool list for other models. -- Permit removals and replacements through the fallback path. - -Purely additive changes must preserve the stable provider prompt/schema prefix when the provider supports native deferred loading. Existing tools must not be renamed or reordered merely because another tool was added. - -Tool descriptions should carry the instructions needed by lazily added tools. Loop should not modify the system prompt when the tool list changes, because doing so can invalidate the provider cache even when deferred tool schemas are supported. - -`setTools()` must be coherent with Loop's materialized executors, payload transforms, headers, and shared resources. It must not update only pi's visible list while leaving an independent Loop runtime stale. - -## Model changes - -The requested tool list remains caller-owned when a model changes. - -```ts -await harness.setModel("openai:gpt-5.6-sol"); -``` - -Loop revalidates the same tool specifications against the new model. It must not silently replace, add, remove, or rename tools. - -An incompatible native tool produces a direct error: - -```text -anthropic browser_20260701 requires an Anthropic model; selected openai:gpt-5.6-sol -``` - -Caller-provided generic tools and compatible Loop-authored tools remain installed. - -## One current tool list - -`tools` defines the current catalog exposed to the model. There is no separate constructor-level installed catalog and active subset. - -The following Loop-facing configuration should be removed: - -```ts -activeToolNames -setActiveTools() -``` - -Callers use `setTools()` for additions, removals, and replacements. Loop may use pi's registration and activation machinery internally to implement deferred loading, but that distinction must not become a second public source of truth in `CuaAgent` or `CuaAgentHarness`. - -The CLI's interactive `/tools` menu is an application-level consumer of exactly this contract, not a second mechanism. It holds the list it composed for the active model as the baseline, and applies a user-selected **subset** of that baseline through one `setTools()` call. It never adds a tool the application did not compose, so it cannot introduce an unsupported tool. Because tool identities are provider-specific, a `/model` change rebuilds the baseline from the new model's defaults and discards the previous selection with an explicit notice — the alternative, re-applying a selection by key across providers, is the silent replacement forbidden under Non-goals. - -## System instructions and descriptions - -Loop should get out of the business of generating default system prompts. - -The model should learn what is available from the exact tool names, descriptions, and schemas it receives. Correctness-critical prerequisites belong in tool descriptions and schemas. - -For example, `browser_act` must explain that ref-based steps require current refs from `browser_snapshot` or `browser_find`, that refs must not be invented, and that navigation may require a fresh snapshot. - -Selecting a provider-native tool or predefined toolset must not silently install the provider's example system prompt. The caller owns the system prompt. - -Loop tool specifications should not contribute `promptSnippet`, `promptGuidelines`, or active-tool-specific system-prompt fragments by default. This keeps tool additions cache-friendly and makes `tools: []` genuinely free of Loop interaction instructions. - -If a correctness requirement cannot be expressed in a tool description or schema, that is a design issue to resolve explicitly before adding system-prompt generation back into scope. - -## Provider support - -Provider support should be validated per requested tool, not per global mode. - -A provider capability description may include: - -- ordinary function-tool support -- provider-native tool support -- accepted JSON Schema features -- tool-name restrictions -- support for mixing native and function tools -- coordinate conventions used by a specific computer tool -- payload-transform composition constraints - -Coordinate uncertainty in one computer tool must not disable coordinate-free browser tools such as snapshots, refs, semantic waits, or action plans. - -Native adapters compose by selected identity: OpenAI replaces only its native computer placeholder, while Google removes only selected native placeholders and preserves unrelated function tools. - -## Removal of `computer_use_extra` - -`computer_use_extra` is deleted entirely: definition, executor, implicit installation, exports, tests, and documentation. - -No replacement navigation helper is added automatically or under a new hidden name. A caller who needs navigation chooses an explicit capability, such as: - -- a provider-native browser tool -- `loop.tools.browser.navigate()` -- `loop.tools.playwright()` -- a caller-provided navigation tool - -An OS-computer-only toolset may still navigate through ordinary keyboard input. Loop should not silently append a separate escape-hatch tool. - -## Error behavior - -Construction, `setTools()`, or model switching should fail with errors that name the requested tools and the violated constraint. - -Examples: - -```text -tool name "browser_act" is requested by both kloop.browser.act and custom.plan -``` - -```text -anthropic browser_20260701 cannot be used with model openai:gpt-5.6-sol -``` - -```text -tools "provider..native.computer" and "provider..native.browser" require conflicting payload transforms for "tools.computer_use" -``` - -```text -provider google does not accept the schema used by "browser_act" -``` - -Loop must not silently drop tools, substitute a different selected toolset, append tools, or rename an existing tool after a dynamic addition. A selected native tool may declare an equivalent function-transport fallback under the same identity, name, schema, and executor for credentials that cannot access the native provider feature; this does not change the caller's tool catalog. - -## Removal of current API - -The following constructor options are removed rather than deprecated: - -```ts -extraTools -mode -nativeTool -playwright -activeToolNames -``` - -The following methods are removed from the Loop-facing API: - -```ts -setMode() -getMode() -setActiveTools() -``` - -`computer_use_extra` and Loop-generated default system prompts are removed with them. - -Their replacements are direct tool-list entries: - -| Current option | Replacement | -| --- | --- | -| `extraTools: [tool]` | include `tool` in `tools` | -| `mode: "computer"` | `tools: loop.toolsets.computer()` or an explicit provider-native list | -| `mode: "browser"` | `tools: loop.toolsets.browser()` or an explicit list | -| `mode: "hybrid"` | compose the desired provider and Loop tools explicitly | -| `nativeTool: spec` | `tools: [loop.providers.anthropic.tools.browser(spec)]` | -| `playwright: true` | `tools: [loop.tools.playwright()]` | -| `activeToolNames` | pass the exact current list and change it with `setTools()` | - -## Documentation requirements - -The implementation updates: - -- `docs/architecture.md` with the tool-spec composition and provider-adapter ownership boundaries -- package READMEs with exact constructor examples and no legacy mode terminology -- API documentation with the definitions of tool, action, and toolset -- user-facing examples for native-only, provider-native plus Loop, Playwright-only, browser-act-only, empty, batch, and dynamic-loading configurations - -Every provider tool surface must expose the first-party source it mirrors. Loop-authored additions must be described as Loop capabilities rather than provider defaults. - -## Implemented design resolutions - -1. **Name composition:** exact and provider-normalized collisions reject; caller aliases/namespaces are explicit; native names are fixed. -2. **Payload transforms:** transforms consume stable identities, declare static write claims, and compose in a fixed phase order. -3. **Result ownership:** each tool returns only requested reads, explicit screenshots, or its own structured semantic feedback. -4. **Batch overlap:** batches are mechanical; `browser_act` remains semantic; browser batches share ref state without a workflow DSL. -5. **Dynamic loading:** `setTools()` uses pi 0.83.0 additive markers only for final, cache-preserving in-tool additions; other changes are eager. -6. **Shared resources:** one resource pool survives tool/model changes and owns the translator and lazy CDP executor. -7. **Provider exports:** the native OpenAI, Anthropic, and Google surfaces are namespaced, cite first-party sources, and are tested against their declared contracts. Meta, xAI, and Moonshot use Loop-authored browser tools; the CLI explicitly appends `browser_act` to the Meta and xAI catalogs. Moonshot is excluded: its API accepts the complex `browser_wait_for` schema but rejects a request carrying `browser_act`'s much larger one, so the catalog gates oversized schemas separately from merely-complex ones. - -## Decisions recorded - -- `tools: []` is valid. -- Loop does not generate a default system prompt. -- `computer_use_extra` is removed with no implicit replacement. -- There is one current public tool list; no Loop-facing `activeToolNames` layer. -- First-party provider-native tools are namespaced separately from Loop-authored tools. -- Tool factories and toolsets are discoverable under a namespace, not exported as many global functions. -- `browser_act` remains outside `loop.toolsets.browser()`; applications may opt - into it explicitly, and the CLI does so for structured Loop-browser catalogs. -- Naming, payload-transform composition, result formatting, and batch overlap must be resolved before code is written. - -## Acceptance criteria - -- Both constructors have one required tool-selection source of truth and accept `tools: []`. -- The current tool-related constructor options, active-tool option, and mode methods are removed. -- `computer_use_extra` and Loop-generated default system prompts are removed. -- Loop-authored and first-party provider-native tools are exposed through distinct, discoverable namespaces. -- Exact native-browser-only, provider-native-plus-Loop, Playwright-only, browser-act-only, and empty configurations are tested. -- No undeclared helper tool is installed. -- `computer_batch` exposes explicit action control, and a browser batch design is resolved and tested. -- Mid-conversation additive tool loading uses provider-native deferred loading where supported and preserves the prompt cache. -- Removals and replacements use a safe fallback and preserve transcript/session correctness. -- Model switching preserves the requested tool catalog or reports a named incompatibility. -- Tool descriptions mention only their own selected capabilities and prerequisites. -- Provider adapters compose explicit tool transformations rather than classify tools by ambiguous names. -- Coordinate conversion and result formatting are tool-owned; screenshots require explicit screenshot or zoom actions. -- Architecture, API, README, and user-facing terminology consistently distinguish tools, actions, and toolsets. diff --git a/docs/architecture.md b/docs/architecture.md index 5815d437..34196268 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -138,9 +138,9 @@ catalog and model changes. It owns: - browser element-ref and frame state; - screenshot and Playwright execution capabilities. -This prevents `setTools()` from resetting refs, tabs, browser state, or caches. -Tools are materialized as small adapters over that shared pool, exactly once -per spec object. +Recompiling and applying a catalog preserves refs, tabs, browser state, and +caches. Tools are materialized as small adapters over that shared pool, +exactly once per spec object. ## Action planes and result feedback diff --git a/package-lock.json b/package-lock.json index 6ffb415c..c72d1c6a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2486,6 +2486,13 @@ "node": ">=14.0.0" } }, + "node_modules/@stablelib/base64": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@stablelib/base64/-/base64-1.0.1.tgz", + "integrity": "sha512-1bnPQqSxSuc3Ii6MhBysoWCg58j97aUjuCSZrGSmDxNqtytIi0k8utUenAwTZN4V5mXXYGsVUI9zeBqy+jBOSQ==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/chai": { "version": "5.2.3", "dev": true, @@ -2881,6 +2888,13 @@ "version": "3.0.2", "license": "MIT" }, + "node_modules/fast-sha256": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/fast-sha256/-/fast-sha256-1.3.0.tgz", + "integrity": "sha512-n11RGP/lrWEFI/bWdygLxhI+pVeo1ZYIVwvvPkW7azl/rOy+F3HYRZ2K5zeE9mmkhQppyv9sQFx0JM9UabnpPQ==", + "dev": true, + "license": "Unlicense" + }, "node_modules/fdir": { "version": "6.5.0", "dev": true, @@ -3537,6 +3551,17 @@ "dev": true, "license": "MIT" }, + "node_modules/standardwebhooks": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/standardwebhooks/-/standardwebhooks-1.0.0.tgz", + "integrity": "sha512-BbHGOQK9olHPMvQNHWul6MYlrRTAOKn03rOe4A8O3CLWhNf4YHBqq2HJKKC+sfqpxiBY52pNeesD6jIiLDz8jg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@stablelib/base64": "^1.0.0", + "fast-sha256": "^1.3.0" + } + }, "node_modules/std-env": { "version": "3.10.0", "dev": true, @@ -4000,7 +4025,9 @@ "typebox": "1.3.7" }, "devDependencies": { + "@anthropic-ai/sdk": "^0.117.1", "@earendil-works/pi-coding-agent": "0.83.0", + "@google/genai": "^2.17.1", "tsdown": "^0.22.2", "vitest": "^3.2.4" }, @@ -4017,6 +4044,53 @@ } } }, + "packages/loop/node_modules/@anthropic-ai/sdk": { + "version": "0.117.1", + "resolved": "https://registry.npmjs.org/@anthropic-ai/sdk/-/sdk-0.117.1.tgz", + "integrity": "sha512-Yn2QlXfyCiKJ5YGCOOay7ZE78ISvII2XY621WMCiflmG8IYgwx59IBwPExxki3Xk9jKUtnD/Sj6UvplWr0rZxg==", + "dev": true, + "license": "MIT", + "dependencies": { + "json-schema-to-ts": "^3.1.1", + "standardwebhooks": "^1.0.0" + }, + "bin": { + "anthropic-ai-sdk": "bin/cli" + }, + "peerDependencies": { + "zod": "^3.25.0 || ^4.0.0" + }, + "peerDependenciesMeta": { + "zod": { + "optional": true + } + } + }, + "packages/loop/node_modules/@google/genai": { + "version": "2.17.1", + "resolved": "https://registry.npmjs.org/@google/genai/-/genai-2.17.1.tgz", + "integrity": "sha512-CdZ3M/titoH81hXXkvOikrOW26bC9IXh9iYT7u+r+5p7wi1LnMEnB0AbJfDeWAkjuneP4oJ299BtCt6twmORWg==", + "dev": true, + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "google-auth-library": "^10.3.0", + "p-retry": "^4.6.2", + "protobufjs": "^7.5.4", + "ws": "^8.18.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "@modelcontextprotocol/sdk": "^1.25.2" + }, + "peerDependenciesMeta": { + "@modelcontextprotocol/sdk": { + "optional": true + } + } + }, "packages/ptywright": { "name": "@onkernel/ptywright", "version": "0.1.0", diff --git a/packages/loop/README.md b/packages/loop/README.md index 75f79e9c..b1048923 100644 --- a/packages/loop/README.md +++ b/packages/loop/README.md @@ -549,8 +549,17 @@ npm test --workspace @onkernel/loop Build before testing: the pi print/RPC test loads the extension the way pi does, through this package's own entry points. -See [`examples/`](examples) for direct catalog/model usage, direct-agent and -harness smoke tests, provider matrices, and the Anthropic-native compositions. +The npm-wired agent and harness examples accept `--model` and `--scenario`: + +```bash +npm run example:agent --workspace @onkernel/loop -- \ + --model anthropic:claude-opus-5 --scenario wikipedia-search +npm run example:harness --workspace @onkernel/loop -- \ + --model google:gemini-3.6-flash --scenario hn-url-and-screenshot +``` + +See [`examples/`](examples) for direct catalog/model usage, these parameterized +agent and harness examples, and the Anthropic-native composition. ## License diff --git a/packages/loop/examples/agent-openai-smoke.ts b/packages/loop/examples/agent-openai-smoke.ts deleted file mode 100644 index 352a3024..00000000 --- a/packages/loop/examples/agent-openai-smoke.ts +++ /dev/null @@ -1,47 +0,0 @@ -import Kernel from "@onkernel/sdk"; -import { loop } from "../src/index"; -import { Agent } from "@earendil-works/pi-agent-core"; -import { attach, type LoopModelRef, requireLoopEnvApiKeyForModel } from "../src/pi/index"; -import { logAgentEvent, logAssistant } from "./shared/logging"; -import { SCENARIOS } from "./shared/scenarios"; - -const modelRef = (process.env.MODEL_REF as LoopModelRef | undefined) ?? "openai:gpt-5.6-sol"; - -async function main(): Promise { - const kernelApiKey = process.env.KERNEL_API_KEY; - if (!kernelApiKey) throw new Error("KERNEL_API_KEY is required"); - requireLoopEnvApiKeyForModel(modelRef); - const client = new Kernel({ apiKey: kernelApiKey }); - const browser = await client.browsers.create({ stealth: true }); - const kb = attach({ browser, client }); - - try { - // Prefer structured browser refs and semantic reads for the OpenAI smoke, - // and opt into verified dependent plans without changing the base toolset. - const compiled = kb.compile({ - model: modelRef, - tools: [...loop.toolsets.browser(), loop.tools.browser.act()], - }); - const agent = new Agent({ - streamFn: (selected, context, options) => compiled.models.streamSimple(selected, context, options), - initialState: { - model: compiled.model, - tools: [...compiled.agentTools], - systemPrompt: "Use the provided computer and browser tools to interact with the page.", - }, - }); - - agent.subscribe(logAgentEvent); - - const scenario = SCENARIOS[0]!; - console.log(`running scenario: ${scenario.name} model=${modelRef}`); - await agent.prompt(scenario.prompt); - const assistant = [...agent.state.messages].reverse().find((message) => message.role === "assistant"); - logAssistant(assistant?.role === "assistant" ? assistant : undefined); - } finally { - await kb.dispose(); - await client.browsers.deleteByID(browser.session_id); - } -} - -void main(); diff --git a/packages/loop/examples/agent-provider-matrix.ts b/packages/loop/examples/agent.ts similarity index 78% rename from packages/loop/examples/agent-provider-matrix.ts rename to packages/loop/examples/agent.ts index f86f45f6..427e8484 100644 --- a/packages/loop/examples/agent-provider-matrix.ts +++ b/packages/loop/examples/agent.ts @@ -1,20 +1,17 @@ import Kernel from "@onkernel/sdk"; import { Agent } from "@earendil-works/pi-agent-core"; -import { attach, type LoopModelRef, requireLoopEnvApiKeyForModel } from "../src/pi/index"; +import { attach, requireLoopEnvApiKeyForModel } from "../src/pi/index"; import { logAgentEvent, logAssistant } from "./shared/logging"; -import { SCENARIOS } from "./shared/scenarios"; +import { parseExampleOptions } from "./shared/options"; import { toolsForModel } from "./shared/tools"; -const modelRef = (process.env.MODEL_REF as LoopModelRef | undefined) ?? "openai:gpt-5.6-sol"; -const scenarioName = process.env.SCENARIO ?? SCENARIOS[0]!.name; - async function main(): Promise { + const { modelRef, scenario } = parseExampleOptions(); const kernelApiKey = process.env.KERNEL_API_KEY; if (!kernelApiKey) throw new Error("KERNEL_API_KEY is required"); requireLoopEnvApiKeyForModel(modelRef); const client = new Kernel({ apiKey: kernelApiKey }); const browser = await client.browsers.create({ stealth: true }); - const scenario = SCENARIOS.find((entry) => entry.name === scenarioName) ?? SCENARIOS[0]!; const kb = attach({ browser, client }); try { @@ -28,6 +25,7 @@ async function main(): Promise { }, }); agent.subscribe(logAgentEvent); + console.log(`model=${modelRef} scenario=${scenario.name}`); await agent.prompt(scenario.prompt); const assistant = [...agent.state.messages].reverse().find((message) => message.role === "assistant"); diff --git a/packages/loop/examples/harness-openai-smoke.ts b/packages/loop/examples/harness-openai-smoke.ts deleted file mode 100644 index 59fe14c3..00000000 --- a/packages/loop/examples/harness-openai-smoke.ts +++ /dev/null @@ -1,55 +0,0 @@ -import Kernel from "@onkernel/sdk"; -import { loop } from "../src/index"; -import { AgentHarness, InMemorySessionRepo } from "@earendil-works/pi-agent-core"; -import { attach, type LoopModelRef, requireLoopEnvApiKeyForModel } from "../src/pi/index"; -import { logAgentEvent, logAssistant } from "./shared/logging"; -import { SCENARIOS } from "./shared/scenarios"; - -const modelRef = (process.env.MODEL_REF as LoopModelRef | undefined) ?? "openai:gpt-5.6-sol"; - -async function main(): Promise { - const kernelApiKey = process.env.KERNEL_API_KEY; - if (!kernelApiKey) throw new Error("KERNEL_API_KEY is required"); - requireLoopEnvApiKeyForModel(modelRef); - const client = new Kernel({ apiKey: kernelApiKey }); - const browser = await client.browsers.create({ stealth: true }); - const kb = attach({ browser, client }); - - try { - const sessionRepo = new InMemorySessionRepo(); - const session = await sessionRepo.create({ id: "harness-openai-smoke" }); - // Prefer structured browser refs and semantic reads for the OpenAI smoke, - // and opt into verified dependent plans without changing the base toolset. - const compiled = kb.compile({ - model: modelRef, - tools: [...loop.toolsets.browser(), loop.tools.browser.act()], - }); - const harness = new AgentHarness({ - session, - model: compiled.model, - models: compiled.models, - tools: [...compiled.tools], - activeToolNames: compiled.tools.map((tool) => tool.name), - systemPrompt: "Use the provided computer and browser tools to interact with the page.", - }); - compiled.activate(harness); - - harness.subscribe(logAgentEvent); - - const scenario = SCENARIOS[0]!; - console.log(`running scenario: ${scenario.name} model=${modelRef}`); - const response = await harness.prompt(scenario.prompt); - const branch = await session.getBranch(); - const lastAssistant = [...branch] - .reverse() - .flatMap((entry) => - entry.type === "message" && entry.message.role === "assistant" ? [entry.message] : [], - )[0]; - logAssistant(lastAssistant ?? response); - } finally { - await kb.dispose(); - await client.browsers.deleteByID(browser.session_id); - } -} - -void main(); diff --git a/packages/loop/examples/harness-provider-matrix.ts b/packages/loop/examples/harness.ts similarity index 74% rename from packages/loop/examples/harness-provider-matrix.ts rename to packages/loop/examples/harness.ts index c758d386..4cdf1714 100644 --- a/packages/loop/examples/harness-provider-matrix.ts +++ b/packages/loop/examples/harness.ts @@ -1,25 +1,21 @@ import Kernel from "@onkernel/sdk"; import { AgentHarness, InMemorySessionRepo } from "@earendil-works/pi-agent-core"; -import { attach, type LoopModelRef, requireLoopEnvApiKeyForModel } from "../src/pi/index"; +import { attach, requireLoopEnvApiKeyForModel } from "../src/pi/index"; import { logAgentEvent, logAssistant } from "./shared/logging"; -import { SCENARIOS } from "./shared/scenarios"; +import { parseExampleOptions } from "./shared/options"; import { toolsForModel } from "./shared/tools"; -const modelRef = (process.env.MODEL_REF as LoopModelRef | undefined) ?? "openai:gpt-5.6-sol"; -const scenarioName = process.env.SCENARIO ?? SCENARIOS[0]!.name; - async function main(): Promise { + const { modelRef, scenario } = parseExampleOptions(); const kernelApiKey = process.env.KERNEL_API_KEY; if (!kernelApiKey) throw new Error("KERNEL_API_KEY is required"); requireLoopEnvApiKeyForModel(modelRef); const client = new Kernel({ apiKey: kernelApiKey }); const browser = await client.browsers.create({ stealth: true }); - const scenario = SCENARIOS.find((entry) => entry.name === scenarioName) ?? SCENARIOS[0]!; const kb = attach({ browser, client }); try { - const sessionRepo = new InMemorySessionRepo(); - const session = await sessionRepo.create({ id: `harness-provider-matrix-${scenario.name}` }); + const session = await new InMemorySessionRepo().create({ id: `harness-${scenario.name}` }); const compiled = kb.compile({ model: modelRef, tools: toolsForModel(modelRef) }); const harness = new AgentHarness({ session, @@ -31,6 +27,7 @@ async function main(): Promise { }); compiled.activate(harness); harness.subscribe(logAgentEvent); + console.log(`model=${modelRef} scenario=${scenario.name}`); const response = await harness.prompt(scenario.prompt); const branch = await session.getBranch(); diff --git a/packages/loop/examples/shared/options.ts b/packages/loop/examples/shared/options.ts new file mode 100644 index 00000000..32f5efba --- /dev/null +++ b/packages/loop/examples/shared/options.ts @@ -0,0 +1,33 @@ +import type { LoopModelRef } from "../../src/pi/index"; +import { SCENARIOS, type BrowserScenario } from "./scenarios"; + +export interface ExampleOptions { + modelRef: LoopModelRef; + scenario: BrowserScenario; +} + +export function parseExampleOptions(argv = process.argv.slice(2)): ExampleOptions { + let modelRef = (process.env.MODEL_REF as LoopModelRef | undefined) ?? "openai:gpt-5.6-sol"; + let scenarioName = process.env.SCENARIO ?? SCENARIOS[0]!.name; + + for (let index = 0; index < argv.length; index += 1) { + const arg = argv[index]; + const value = argv[index + 1]; + if (arg === "--model" && value) { + modelRef = value as LoopModelRef; + index += 1; + } else if (arg === "--scenario" && value) { + scenarioName = value; + index += 1; + } else { + throw new Error(`Unknown or incomplete option: ${arg}`); + } + } + + const scenario = SCENARIOS.find((entry) => entry.name === scenarioName); + if (!scenario) { + const available = SCENARIOS.map((entry) => entry.name).join(", "); + throw new Error(`Unknown scenario ${JSON.stringify(scenarioName)}. Choose one of: ${available}`); + } + return { modelRef, scenario }; +} diff --git a/packages/loop/package.json b/packages/loop/package.json index 08f548a4..dfda2a16 100644 --- a/packages/loop/package.json +++ b/packages/loop/package.json @@ -57,8 +57,8 @@ "typecheck": "tsc -b", "clean": "tsc -b --clean && rm -rf dist dist-published dist-tsc", "example:quickstart": "NODE_OPTIONS=--conditions=source tsx examples/quickstart.ts", - "example:agent": "NODE_OPTIONS=--conditions=source tsx examples/agent-openai-smoke.ts", - "example:harness": "NODE_OPTIONS=--conditions=source tsx examples/harness-openai-smoke.ts", + "example:agent": "NODE_OPTIONS=--conditions=source tsx examples/agent.ts", + "example:harness": "NODE_OPTIONS=--conditions=source tsx examples/harness.ts", "test": "vitest --run", "test:integration": "vitest --run --config vitest.integration.config.ts" }, @@ -80,7 +80,9 @@ } }, "devDependencies": { + "@anthropic-ai/sdk": "^0.117.1", "@earendil-works/pi-coding-agent": "0.83.0", + "@google/genai": "^2.17.1", "tsdown": "^0.22.2", "vitest": "^3.2.4" } diff --git a/packages/loop/src/core/actions/computer.ts b/packages/loop/src/core/actions/computer.ts index 5e6aa9d2..2388193f 100644 --- a/packages/loop/src/core/actions/computer.ts +++ b/packages/loop/src/core/actions/computer.ts @@ -30,15 +30,6 @@ export const COMPUTER_ACTION_TYPES = [ export type ComputerActionType = (typeof COMPUTER_ACTION_TYPES)[number]; -/** - * The default computer-mode toolset. This is the pre-modes canonical action list: - * every computer action except `zoom`, which is only exposed by default in hybrid - * mode and by Anthropic's native computer tool (`enable_zoom`). - */ -export const DEFAULT_COMPUTER_ACTION_TYPES = COMPUTER_ACTION_TYPES.filter( - (action): action is Exclude => action !== "zoom", -); - /** * Mouse buttons accepted by click, mouse_down, and mouse_up actions. The * executor coerces anything outside this set to "left". @@ -297,5 +288,3 @@ export const COMPUTER_ACTION_SCHEMA_BY_TYPE = { url: Type.Object({ type: Type.Literal("url") }, { additionalProperties: false }), cursor_position: Type.Object({ type: Type.Literal("cursor_position") }, { additionalProperties: false }), } satisfies Record; - -export type ZoomRegion = ComputerActionZoom["region"]; diff --git a/packages/loop/src/core/actions/index.ts b/packages/loop/src/core/actions/index.ts index 943b1922..bd12012c 100644 --- a/packages/loop/src/core/actions/index.ts +++ b/packages/loop/src/core/actions/index.ts @@ -1,47 +1,15 @@ -import type { TSchema } from "typebox"; -import { BROWSER_ACTION_TYPES, createBrowserActionSchemaByType, type BrowserAction, type BrowserActionType, type BrowserActionSchemaOptions } from "./browser"; -import { COMPUTER_ACTION_SCHEMA_BY_TYPE, COMPUTER_ACTION_TYPES, type ComputerAction, type ComputerActionType } from "./computer"; +import { BROWSER_ACTION_TYPES, type BrowserAction } from "./browser"; +import type { ComputerAction } from "./computer"; export * from "./browser"; export * from "./computer"; -/** Any canonical action type, across the computer and browser planes. */ -export type ComputerUseActionType = ComputerActionType | BrowserActionType; - /** Any canonical action, across the computer and browser planes. */ export type ComputerUseAction = ComputerAction | BrowserAction; -/** Every canonical action type: the computer plane followed by the browser plane. */ -export const COMPUTER_USE_ACTION_TYPES: readonly ComputerUseActionType[] = [...COMPUTER_ACTION_TYPES, ...BROWSER_ACTION_TYPES]; - -const COMPUTER_ACTION_TYPE_SET: ReadonlySet = new Set(COMPUTER_ACTION_TYPES); const BROWSER_ACTION_TYPE_SET: ReadonlySet = new Set(BROWSER_ACTION_TYPES); -/** Whether a canonical action type belongs to the computer plane. */ -export function isComputerActionType(action: ComputerUseActionType): action is ComputerActionType { - return COMPUTER_ACTION_TYPE_SET.has(action); -} - -/** Whether a canonical action type belongs to the browser plane. */ -export function isBrowserActionType(action: ComputerUseActionType): action is BrowserActionType { - return BROWSER_ACTION_TYPE_SET.has(action); -} - /** Whether a canonical action belongs to the browser plane. */ export function isBrowserAction(action: ComputerUseAction): action is BrowserAction { return BROWSER_ACTION_TYPE_SET.has(action.type); } - -/** Options for building canonical action schemas. */ -export interface ComputerUseActionSchemaOptions { - /** browser-plane schema variants; see {@link BrowserActionSchemaOptions}. Defaults to coordinates allowed. */ - browser?: BrowserActionSchemaOptions; -} - -/** Build the full action-type → schema map for a schema-options combination. */ -export function computerUseActionSchemaByType(options: ComputerUseActionSchemaOptions = {}): Record { - return { - ...COMPUTER_ACTION_SCHEMA_BY_TYPE, - ...createBrowserActionSchemaByType(options.browser ?? { coordinates: true }), - }; -} diff --git a/packages/loop/src/pi/index.ts b/packages/loop/src/pi/index.ts index 5f1081d6..e44fef55 100644 --- a/packages/loop/src/pi/index.ts +++ b/packages/loop/src/pi/index.ts @@ -13,12 +13,10 @@ export { export type { LoopSimpleStreamOptions, ResponseThreadingOptions, - ResponsesThreadingOptions, } from "./providers/common"; export { responseThreadingDelta, responseThreadingEnabled, - threadResponsesRequest, } from "./providers/common"; export { attach } from "./attach"; export type { diff --git a/packages/loop/src/pi/providers.ts b/packages/loop/src/pi/providers.ts index 6bd03b8e..15187b9f 100644 --- a/packages/loop/src/pi/providers.ts +++ b/packages/loop/src/pi/providers.ts @@ -37,10 +37,6 @@ import { OPENAI_COMPUTER_USE_API, requiresOpenAINamespaceAdapter, streamOpenAICo * Responses transport, and the catalog supplies its serial-tool-call field. * - `moonshotai` is pi's builtin provider untouched: Kimi streams through the * plain OpenAI-compatible chat completions transport with `MOONSHOT_API_KEY`. - * - `meta` is a Loop-only provider pi does not ship. It speaks the OpenAI - * Responses wire protocol, so it registers pi's builtin transport against - * Meta's base URL and credentials. - * * Each call returns an independent collection; register additional providers * or credentials on it freely. Use {@link loopModels} for the shared default. */ diff --git a/packages/loop/src/pi/providers/common.ts b/packages/loop/src/pi/providers/common.ts index 5546e0dc..a1aab439 100644 --- a/packages/loop/src/pi/providers/common.ts +++ b/packages/loop/src/pi/providers/common.ts @@ -1,11 +1,9 @@ import type { Api, AssistantMessage, - Context, Message, Model, SimpleStreamOptions, - StreamOptions, } from "@earendil-works/pi-ai"; import type { LoopIncomingToolPlan } from "../../core/tool-catalog"; @@ -20,32 +18,6 @@ export interface ResponseThreadingOptions { disableResponseThreading?: boolean; } -type ResponsesOnPayload = NonNullable; - -export interface ResponsesThreadingOptions extends ResponseThreadingOptions { - onPayload?: ResponsesOnPayload; -} - -/** Prepare a Responses request using the latest valid stored response id. */ -export function threadResponsesRequest( - context: Context, - api: Api, - options: ResponsesThreadingOptions | undefined, -): { context: Context; onPayload: ResponsesOnPayload; previousResponseId?: string } { - const delta = responseThreadingEnabled(options) ? responseThreadingDelta(context.messages, api) : undefined; - const previousResponseId = delta?.previousResponseId; - const messages = previousResponseId && delta ? delta.deltaMessages : context.messages; - const onPayload: ResponsesOnPayload = async (payload, model) => { - const threaded = { - ...(payload as Record), - store: true, - ...(previousResponseId ? { previous_response_id: previousResponseId } : {}), - }; - return options?.onPayload ? ((await options.onPayload(threaded, model)) ?? threaded) : threaded; - }; - return { context: messages === context.messages ? context : { ...context, messages }, onPayload, previousResponseId }; -} - export function responseThreadingEnabled(options?: ResponseThreadingOptions): boolean { return options?.disableResponseThreading !== true; } diff --git a/packages/loop/test/api-keys.test.ts b/packages/loop/test/api-keys.test.ts index e1323438..ecc04433 100644 --- a/packages/loop/test/api-keys.test.ts +++ b/packages/loop/test/api-keys.test.ts @@ -12,7 +12,6 @@ const ENV_KEYS = [ "ANTHROPIC_API_KEY", "GOOGLE_API_KEY", "GEMINI_API_KEY", - "META_API_KEY", "XAI_API_KEY", "MOONSHOT_API_KEY", "OPENROUTER_API_KEY", @@ -58,8 +57,4 @@ describe("loop api key helpers", () => { process.env.OPENROUTER_API_KEY = "openrouter"; expect(getLoopEnvApiKeyForModel("openrouter:moonshotai/kimi-k3")).toBe("openrouter"); }); - - it("throws readable errors when missing", () => { - delete process.env.META_API_KEY; - }); }); diff --git a/packages/loop/test/example-provider-matrix.test.ts b/packages/loop/test/example-tool-policy.test.ts similarity index 59% rename from packages/loop/test/example-provider-matrix.test.ts rename to packages/loop/test/example-tool-policy.test.ts index 8ae5219a..784f907f 100644 --- a/packages/loop/test/example-provider-matrix.test.ts +++ b/packages/loop/test/example-tool-policy.test.ts @@ -1,15 +1,14 @@ import type { LoopModelRef } from "../src/pi/index"; import { compileLoopToolCatalog } from "../src/index"; import { describe, expect, it } from "vitest"; +import { parseExampleOptions } from "../examples/shared/options"; import { toolsForModel } from "../examples/shared/tools"; /** - * The example matrices are plain scripts: they are excluded from `tsc -b` and are - * never executed in CI, so a provider policy that no longer compiles used to be - * invisible until someone ran the script against a live key. - * - * Limited to models the registry can resolve, so Anthropic's older non-native - * fallback branch is covered by the tool menu's availability tests instead. + * The parameterized agent and harness examples share this tool policy. Keep it + * compilable for every advertised model without requiring live provider keys. + * Anthropic's older non-native fallback branch is covered by the tool menu's + * availability tests instead. */ const models: readonly LoopModelRef[] = [ "openai:gpt-5.6-sol", @@ -22,8 +21,21 @@ const models: readonly LoopModelRef[] = [ "openrouter:moonshotai/kimi-k3", ]; -describe("example provider matrix tool policy", () => { - it("compiles a valid catalog for every model the matrices advertise", () => { +describe("parameterized examples", () => { + it("accepts model and scenario flags", () => { + const options = parseExampleOptions([ + "--model", "anthropic:claude-opus-5", + "--scenario", "wikipedia-search", + ]); + expect(options.modelRef).toBe("anthropic:claude-opus-5"); + expect(options.scenario.name).toBe("wikipedia-search"); + }); + + it("rejects unknown scenarios before provisioning a browser", () => { + expect(() => parseExampleOptions(["--scenario", "missing"])).toThrow("Unknown scenario"); + }); + + it("compiles a valid catalog for every advertised model", () => { for (const model of models) { expect( () => compileLoopToolCatalog({ model, requestedTools: toolsForModel(model) }),