@try-works/dsh-model-as-tool 0.1.2 → 0.3.6
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/bin/dsh-tool-mode-credentials.mjs +20 -0
- package/bin/dsh-tool-mode.mjs +202 -0
- package/catalog/desert-ant-models.json +693 -693
- package/client/client.model-tools.js +592 -0
- package/client/index.js +1679 -0
- package/lib/adapters/index.d.ts.map +1 -1
- package/lib/adapters/index.js +4 -4
- package/lib/bindings.d.ts.map +1 -1
- package/lib/bindings.js +26 -25
- package/lib/capability.d.ts.map +1 -1
- package/lib/capability.js +17 -0
- package/lib/capture-path.d.ts +97 -0
- package/lib/capture-path.d.ts.map +1 -0
- package/lib/capture-path.js +152 -0
- package/lib/diagnostics.d.ts +166 -0
- package/lib/diagnostics.d.ts.map +1 -0
- package/lib/diagnostics.js +135 -0
- package/lib/extension-dependencies.d.ts +197 -0
- package/lib/extension-dependencies.d.ts.map +1 -0
- package/lib/extension-dependencies.js +369 -0
- package/lib/extension-dispatch.d.ts +117 -0
- package/lib/extension-dispatch.d.ts.map +1 -0
- package/lib/extension-dispatch.js +124 -0
- package/lib/extension-registrar.d.ts +100 -0
- package/lib/extension-registrar.d.ts.map +1 -0
- package/lib/extension-registrar.js +134 -0
- package/lib/extensions-api.d.ts +217 -0
- package/lib/extensions-api.d.ts.map +1 -0
- package/lib/extensions-api.js +17 -0
- package/lib/extensions-remote.d.ts +54 -0
- package/lib/extensions-remote.d.ts.map +1 -0
- package/lib/extensions-remote.js +66 -0
- package/lib/extensions-service.d.ts +334 -0
- package/lib/extensions-service.d.ts.map +1 -0
- package/lib/extensions-service.js +1351 -0
- package/lib/harness-llm.d.ts +138 -0
- package/lib/harness-llm.d.ts.map +1 -0
- package/lib/harness-llm.js +218 -0
- package/lib/harness-serving.d.ts +95 -0
- package/lib/harness-serving.d.ts.map +1 -0
- package/lib/harness-serving.js +157 -0
- package/lib/headless.d.ts +97 -0
- package/lib/headless.d.ts.map +1 -0
- package/lib/headless.js +349 -0
- package/lib/http-acquisition.d.ts +27 -0
- package/lib/http-acquisition.d.ts.map +1 -0
- package/lib/http-acquisition.js +136 -0
- package/lib/index.d.ts +250 -7
- package/lib/index.d.ts.map +1 -1
- package/lib/index.js +2625 -70
- package/lib/invocation-accounting.d.ts +169 -0
- package/lib/invocation-accounting.d.ts.map +1 -0
- package/lib/invocation-accounting.js +148 -0
- package/lib/legacy-migration.d.ts +95 -0
- package/lib/legacy-migration.d.ts.map +1 -0
- package/lib/legacy-migration.js +429 -0
- package/lib/managed-workers.d.ts +151 -0
- package/lib/managed-workers.d.ts.map +1 -0
- package/lib/managed-workers.js +249 -0
- package/lib/mcp/client-grant.d.ts +74 -0
- package/lib/mcp/client-grant.d.ts.map +1 -0
- package/lib/mcp/client-grant.js +60 -0
- package/lib/mcp/credential-cli.d.ts +38 -0
- package/lib/mcp/credential-cli.d.ts.map +1 -0
- package/lib/mcp/credential-cli.js +148 -0
- package/lib/mcp/credential-store.d.ts +143 -0
- package/lib/mcp/credential-store.d.ts.map +1 -0
- package/lib/mcp/credential-store.js +454 -0
- package/lib/mcp/host-binding.d.ts +25 -0
- package/lib/mcp/host-binding.d.ts.map +1 -1
- package/lib/mcp/host-binding.js +107 -14
- package/lib/mcp/http.d.ts +68 -18
- package/lib/mcp/http.d.ts.map +1 -1
- package/lib/mcp/http.js +390 -62
- package/lib/mcp/oauth.d.ts +149 -0
- package/lib/mcp/oauth.d.ts.map +1 -0
- package/lib/mcp/oauth.js +396 -0
- package/lib/mcp/protocol.d.ts +46 -7
- package/lib/mcp/protocol.d.ts.map +1 -1
- package/lib/mcp/protocol.js +63 -16
- package/lib/mcp/rate-limit.d.ts +68 -0
- package/lib/mcp/rate-limit.d.ts.map +1 -0
- package/lib/mcp/rate-limit.js +114 -0
- package/lib/mcp/sdk-v2.d.ts +17 -0
- package/lib/mcp/sdk-v2.d.ts.map +1 -0
- package/lib/mcp/sdk-v2.js +181 -0
- package/lib/mcp/server.d.ts +11 -1
- package/lib/mcp/server.d.ts.map +1 -1
- package/lib/mcp/server.js +80 -13
- package/lib/mcp/stdio.d.ts.map +1 -1
- package/lib/mcp/stdio.js +9 -1
- package/lib/mcp/subscriptions.d.ts +43 -1
- package/lib/mcp/subscriptions.d.ts.map +1 -1
- package/lib/mcp/subscriptions.js +90 -9
- package/lib/model-qualification.d.ts +192 -0
- package/lib/model-qualification.d.ts.map +1 -0
- package/lib/model-qualification.js +178 -0
- package/lib/planning.d.ts +246 -0
- package/lib/planning.d.ts.map +1 -0
- package/lib/planning.js +457 -0
- package/lib/prepared-assets.d.ts +178 -0
- package/lib/prepared-assets.d.ts.map +1 -0
- package/lib/prepared-assets.js +286 -0
- package/lib/provider-bridge.d.ts +37 -0
- package/lib/provider-bridge.d.ts.map +1 -1
- package/lib/provider-bridge.js +189 -12
- package/lib/ptc-authority.d.ts +140 -0
- package/lib/ptc-authority.d.ts.map +1 -0
- package/lib/ptc-authority.js +216 -0
- package/lib/ptc-runtime.d.ts +156 -0
- package/lib/ptc-runtime.d.ts.map +1 -0
- package/lib/ptc-runtime.js +191 -0
- package/lib/publication.d.ts +2 -1
- package/lib/publication.d.ts.map +1 -1
- package/lib/publication.js +13 -5
- package/lib/receipt.d.ts.map +1 -1
- package/lib/receipt.js +8 -0
- package/lib/title-proposal.d.ts +194 -0
- package/lib/title-proposal.d.ts.map +1 -0
- package/lib/title-proposal.js +244 -0
- package/lib/tool-mode-run.d.ts +235 -0
- package/lib/tool-mode-run.d.ts.map +1 -0
- package/lib/tool-mode-run.js +283 -0
- package/lib/types.d.ts +3 -1
- package/lib/types.d.ts.map +1 -1
- package/lib/update-surface.d.ts +79 -0
- package/lib/update-surface.d.ts.map +1 -0
- package/lib/update-surface.js +224 -0
- package/node_modules/@try-works/tool-mode-contracts/lib/contracts/lang-detect.d.ts +16 -0
- package/node_modules/@try-works/tool-mode-contracts/lib/contracts/lang-detect.d.ts.map +1 -0
- package/node_modules/@try-works/tool-mode-contracts/lib/contracts/lang-detect.js +85 -0
- package/node_modules/@try-works/tool-mode-contracts/lib/contracts/standard.d.ts +38 -0
- package/node_modules/@try-works/tool-mode-contracts/lib/contracts/standard.d.ts.map +1 -0
- package/node_modules/@try-works/tool-mode-contracts/lib/contracts/standard.js +445 -0
- package/node_modules/@try-works/tool-mode-contracts/lib/contracts/stt-transcribe.d.ts +20 -0
- package/node_modules/@try-works/tool-mode-contracts/lib/contracts/stt-transcribe.d.ts.map +1 -0
- package/node_modules/@try-works/tool-mode-contracts/lib/contracts/stt-transcribe.js +112 -0
- package/node_modules/@try-works/tool-mode-contracts/lib/contracts/text-embed.d.ts +14 -0
- package/node_modules/@try-works/tool-mode-contracts/lib/contracts/text-embed.d.ts.map +1 -0
- package/node_modules/@try-works/tool-mode-contracts/lib/contracts/text-embed.js +63 -0
- package/node_modules/@try-works/tool-mode-contracts/lib/contracts/text-rerank.d.ts +14 -0
- package/node_modules/@try-works/tool-mode-contracts/lib/contracts/text-rerank.d.ts.map +1 -0
- package/node_modules/@try-works/tool-mode-contracts/lib/contracts/text-rerank.js +76 -0
- package/node_modules/@try-works/tool-mode-contracts/lib/dsh.d.ts +82 -0
- package/node_modules/@try-works/tool-mode-contracts/lib/dsh.d.ts.map +1 -0
- package/node_modules/@try-works/tool-mode-contracts/lib/dsh.js +274 -0
- package/node_modules/@try-works/tool-mode-contracts/lib/index.d.ts +26 -0
- package/node_modules/@try-works/tool-mode-contracts/lib/index.d.ts.map +1 -0
- package/node_modules/@try-works/tool-mode-contracts/lib/index.js +48 -0
- package/node_modules/@try-works/tool-mode-contracts/lib/meta.d.ts +104 -0
- package/node_modules/@try-works/tool-mode-contracts/lib/meta.d.ts.map +1 -0
- package/node_modules/@try-works/tool-mode-contracts/lib/meta.js +55 -0
- package/node_modules/@try-works/tool-mode-contracts/lib/registry.d.ts +107 -0
- package/node_modules/@try-works/tool-mode-contracts/lib/registry.d.ts.map +1 -0
- package/node_modules/@try-works/tool-mode-contracts/lib/registry.js +246 -0
- package/node_modules/@try-works/tool-mode-contracts/package.json +34 -0
- package/node_modules/@try-works/tool-mode-extension-sdk/bin/dsh-tool-mode.mjs +20 -0
- package/node_modules/@try-works/tool-mode-extension-sdk/lib/cli.d.ts +80 -0
- package/node_modules/@try-works/tool-mode-extension-sdk/lib/cli.d.ts.map +1 -0
- package/node_modules/@try-works/tool-mode-extension-sdk/lib/cli.js +533 -0
- package/node_modules/@try-works/tool-mode-extension-sdk/lib/declarative-recipes.d.ts +83 -0
- package/node_modules/@try-works/tool-mode-extension-sdk/lib/declarative-recipes.d.ts.map +1 -0
- package/node_modules/@try-works/tool-mode-extension-sdk/lib/declarative-recipes.js +125 -0
- package/node_modules/@try-works/tool-mode-extension-sdk/lib/discovery.d.ts +70 -0
- package/node_modules/@try-works/tool-mode-extension-sdk/lib/discovery.d.ts.map +1 -0
- package/node_modules/@try-works/tool-mode-extension-sdk/lib/discovery.js +201 -0
- package/node_modules/@try-works/tool-mode-extension-sdk/lib/errors.d.ts +57 -0
- package/node_modules/@try-works/tool-mode-extension-sdk/lib/errors.d.ts.map +1 -0
- package/node_modules/@try-works/tool-mode-extension-sdk/lib/errors.js +83 -0
- package/node_modules/@try-works/tool-mode-extension-sdk/lib/index.d.ts +31 -0
- package/node_modules/@try-works/tool-mode-extension-sdk/lib/index.d.ts.map +1 -0
- package/node_modules/@try-works/tool-mode-extension-sdk/lib/index.js +31 -0
- package/node_modules/@try-works/tool-mode-extension-sdk/lib/install/archive.d.ts +86 -0
- package/node_modules/@try-works/tool-mode-extension-sdk/lib/install/archive.d.ts.map +1 -0
- package/node_modules/@try-works/tool-mode-extension-sdk/lib/install/archive.js +185 -0
- package/node_modules/@try-works/tool-mode-extension-sdk/lib/install/dependencies.d.ts +126 -0
- package/node_modules/@try-works/tool-mode-extension-sdk/lib/install/dependencies.d.ts.map +1 -0
- package/node_modules/@try-works/tool-mode-extension-sdk/lib/install/dependencies.js +297 -0
- package/node_modules/@try-works/tool-mode-extension-sdk/lib/install/lifecycle.d.ts +216 -0
- package/node_modules/@try-works/tool-mode-extension-sdk/lib/install/lifecycle.d.ts.map +1 -0
- package/node_modules/@try-works/tool-mode-extension-sdk/lib/install/lifecycle.js +800 -0
- package/node_modules/@try-works/tool-mode-extension-sdk/lib/install/migration.d.ts +210 -0
- package/node_modules/@try-works/tool-mode-extension-sdk/lib/install/migration.d.ts.map +1 -0
- package/node_modules/@try-works/tool-mode-extension-sdk/lib/install/migration.js +368 -0
- package/node_modules/@try-works/tool-mode-extension-sdk/lib/install/runtime-link.d.ts +63 -0
- package/node_modules/@try-works/tool-mode-extension-sdk/lib/install/runtime-link.d.ts.map +1 -0
- package/node_modules/@try-works/tool-mode-extension-sdk/lib/install/runtime-link.js +149 -0
- package/node_modules/@try-works/tool-mode-extension-sdk/lib/install/source-directory.d.ts +35 -0
- package/node_modules/@try-works/tool-mode-extension-sdk/lib/install/source-directory.d.ts.map +1 -0
- package/node_modules/@try-works/tool-mode-extension-sdk/lib/install/source-directory.js +179 -0
- package/node_modules/@try-works/tool-mode-extension-sdk/lib/install/staged.d.ts +179 -0
- package/node_modules/@try-works/tool-mode-extension-sdk/lib/install/staged.d.ts.map +1 -0
- package/node_modules/@try-works/tool-mode-extension-sdk/lib/install/staged.js +235 -0
- package/node_modules/@try-works/tool-mode-extension-sdk/lib/install/store.d.ts +367 -0
- package/node_modules/@try-works/tool-mode-extension-sdk/lib/install/store.d.ts.map +1 -0
- package/node_modules/@try-works/tool-mode-extension-sdk/lib/install/store.js +807 -0
- package/node_modules/@try-works/tool-mode-extension-sdk/lib/install/update-flow.d.ts +185 -0
- package/node_modules/@try-works/tool-mode-extension-sdk/lib/install/update-flow.d.ts.map +1 -0
- package/node_modules/@try-works/tool-mode-extension-sdk/lib/install/update-flow.js +504 -0
- package/node_modules/@try-works/tool-mode-extension-sdk/lib/install/zip-source.d.ts +17 -0
- package/node_modules/@try-works/tool-mode-extension-sdk/lib/install/zip-source.d.ts.map +1 -0
- package/node_modules/@try-works/tool-mode-extension-sdk/lib/install/zip-source.js +80 -0
- package/node_modules/@try-works/tool-mode-extension-sdk/lib/load.d.ts +52 -0
- package/node_modules/@try-works/tool-mode-extension-sdk/lib/load.d.ts.map +1 -0
- package/node_modules/@try-works/tool-mode-extension-sdk/lib/load.js +102 -0
- package/node_modules/@try-works/tool-mode-extension-sdk/lib/manifest.d.ts +171 -0
- package/node_modules/@try-works/tool-mode-extension-sdk/lib/manifest.d.ts.map +1 -0
- package/node_modules/@try-works/tool-mode-extension-sdk/lib/manifest.js +271 -0
- package/node_modules/@try-works/tool-mode-extension-sdk/lib/runtime.d.ts +242 -0
- package/node_modules/@try-works/tool-mode-extension-sdk/lib/runtime.d.ts.map +1 -0
- package/node_modules/@try-works/tool-mode-extension-sdk/lib/runtime.js +58 -0
- package/node_modules/@try-works/tool-mode-extension-sdk/lib/state.d.ts +118 -0
- package/node_modules/@try-works/tool-mode-extension-sdk/lib/state.d.ts.map +1 -0
- package/node_modules/@try-works/tool-mode-extension-sdk/lib/state.js +113 -0
- package/node_modules/@try-works/tool-mode-extension-sdk/lib/worker/worker-child.d.ts +2 -0
- package/node_modules/@try-works/tool-mode-extension-sdk/lib/worker/worker-child.d.ts.map +1 -0
- package/node_modules/@try-works/tool-mode-extension-sdk/lib/worker/worker-child.js +351 -0
- package/node_modules/@try-works/tool-mode-extension-sdk/lib/worker.d.ts +150 -0
- package/node_modules/@try-works/tool-mode-extension-sdk/lib/worker.d.ts.map +1 -0
- package/node_modules/@try-works/tool-mode-extension-sdk/lib/worker.js +670 -0
- package/node_modules/@try-works/tool-mode-extension-sdk/package.json +53 -0
- package/node_modules/@try-works/tool-mode-extension-sdk/schema/extension-manifest-1.1.0.schema.json +253 -0
- package/node_modules/@try-works/tool-mode-recipes/bin/tool-mode-recipe.mjs +15 -0
- package/node_modules/@try-works/tool-mode-recipes/lib/broker.d.ts +427 -0
- package/node_modules/@try-works/tool-mode-recipes/lib/broker.d.ts.map +1 -0
- package/node_modules/@try-works/tool-mode-recipes/lib/broker.js +592 -0
- package/node_modules/@try-works/tool-mode-recipes/lib/cli.d.ts +60 -0
- package/node_modules/@try-works/tool-mode-recipes/lib/cli.d.ts.map +1 -0
- package/node_modules/@try-works/tool-mode-recipes/lib/cli.js +936 -0
- package/node_modules/@try-works/tool-mode-recipes/lib/dsh-adapter.d.ts +237 -0
- package/node_modules/@try-works/tool-mode-recipes/lib/dsh-adapter.d.ts.map +1 -0
- package/node_modules/@try-works/tool-mode-recipes/lib/dsh-adapter.js +200 -0
- package/node_modules/@try-works/tool-mode-recipes/lib/fixture-provider.d.ts +85 -0
- package/node_modules/@try-works/tool-mode-recipes/lib/fixture-provider.d.ts.map +1 -0
- package/node_modules/@try-works/tool-mode-recipes/lib/fixture-provider.js +176 -0
- package/node_modules/@try-works/tool-mode-recipes/lib/images.d.ts +105 -0
- package/node_modules/@try-works/tool-mode-recipes/lib/images.d.ts.map +1 -0
- package/node_modules/@try-works/tool-mode-recipes/lib/images.js +129 -0
- package/node_modules/@try-works/tool-mode-recipes/lib/index.d.ts +25 -0
- package/node_modules/@try-works/tool-mode-recipes/lib/index.d.ts.map +1 -0
- package/node_modules/@try-works/tool-mode-recipes/lib/index.js +25 -0
- package/node_modules/@try-works/tool-mode-recipes/lib/recipe-format.d.ts +325 -0
- package/node_modules/@try-works/tool-mode-recipes/lib/recipe-format.d.ts.map +1 -0
- package/node_modules/@try-works/tool-mode-recipes/lib/recipe-format.js +1310 -0
- package/node_modules/@try-works/tool-mode-recipes/lib/templates.d.ts +51 -0
- package/node_modules/@try-works/tool-mode-recipes/lib/templates.d.ts.map +1 -0
- package/node_modules/@try-works/tool-mode-recipes/lib/templates.js +310 -0
- package/node_modules/@try-works/tool-mode-recipes/lib/titles.d.ts +227 -0
- package/node_modules/@try-works/tool-mode-recipes/lib/titles.d.ts.map +1 -0
- package/node_modules/@try-works/tool-mode-recipes/lib/titles.js +407 -0
- package/node_modules/@try-works/tool-mode-recipes/package.json +56 -0
- package/node_modules/@try-works/tool-mode-recipes/recipes/describe-image.json +38 -0
- package/node_modules/@try-works/tool-mode-recipes/recipes/text-title.json +45 -0
- package/node_modules/@try-works/tool-mode-recipes/schema/harness-model-recipe.schema.json +273 -0
- package/package.json +50 -17
- package/remote/spec.mjs +439 -0
- package/remote/typert.host.mjs +14 -0
- package/remote/typert.remote-client.mjs +16 -0
- package/lib/adapters/tongue.d.ts +0 -31
- package/lib/adapters/tongue.d.ts.map +0 -1
- package/lib/adapters/tongue.js +0 -93
- package/lib/adapters/voz.d.ts +0 -28
- package/lib/adapters/voz.d.ts.map +0 -1
- package/lib/adapters/voz.js +0 -102
- /package/{lib → node_modules/@try-works/tool-mode-extension-sdk/lib}/licensing.d.ts +0 -0
- /package/{lib → node_modules/@try-works/tool-mode-extension-sdk/lib}/licensing.d.ts.map +0 -0
- /package/{lib → node_modules/@try-works/tool-mode-extension-sdk/lib}/licensing.js +0 -0
|
@@ -0,0 +1,592 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The DSH LLM broker: one provider-neutral harness executor for declarative recipes
|
|
3
|
+
* (HLM-01, HLM-02, HLM-05, HLM-06, HLM-07, HLM-09, HLM-10, HLM-11).
|
|
4
|
+
*
|
|
5
|
+
* The broker's whole job is to build ONE request that contains nothing but the recipe's
|
|
6
|
+
* instructions and the validated bound inputs, and then to report honestly what came
|
|
7
|
+
* back. Every invariant here is a way the request could stop being that request, or the
|
|
8
|
+
* result stop being the result:
|
|
9
|
+
*
|
|
10
|
+
* - **The request is isolated by construction.** It is built from the recipe and the
|
|
11
|
+
* bound values alone. There is no parameter through which parent history, a parent
|
|
12
|
+
* system prompt, a tool catalog, skills, attachments or memory could arrive, and the
|
|
13
|
+
* shape is frozen so nothing can be added to it after validation (HLM-05, AT-68).
|
|
14
|
+
* - **Images are typed media, or the call is refused.** An image recipe on a provider
|
|
15
|
+
* whose capabilities do not cover images fails with `IMAGE_INPUT_UNAVAILABLE`. It does
|
|
16
|
+
* not quietly degrade to a text-only call that "describes" an image the model never
|
|
17
|
+
* received — that fallback produces a fluent invention, which is worse than an error
|
|
18
|
+
* (HLM-11).
|
|
19
|
+
* - **An unknown capability is not permission.** A provider that has not reported its
|
|
20
|
+
* image support fails with `EXECUTOR_CAPABILITY_UNKNOWN` rather than being assumed
|
|
21
|
+
* capable (MODEL-06's three-state rule, reused here).
|
|
22
|
+
* - **Remote is a policy decision, not an implementation detail.** `permitsDestination`
|
|
23
|
+
* is mirrored from RES-10 so a local-only profile refuses a remote provider however
|
|
24
|
+
* the tool was built.
|
|
25
|
+
* - **Cancellation is reported as cancellation.** An aborted signal can never produce a
|
|
26
|
+
* success, and never an empty one: an empty success is exactly what a caller writes
|
|
27
|
+
* into a document (HLM-10).
|
|
28
|
+
* - **Refusal, truncation, an empty completion and a stray tool call are errors, each
|
|
29
|
+
* with its own code.** HLM-07 requires them to be distinct failures, because a caller
|
|
30
|
+
* that cannot tell "the model refused" from "the model was cut off" cannot respond
|
|
31
|
+
* correctly to either (RCP-11).
|
|
32
|
+
* - **Recording is a precondition, not a side effect.** A recipe that declines recording
|
|
33
|
+
* under a host that requires it is refused with `CAPTURE_POLICY_CONFLICT` BEFORE the
|
|
34
|
+
* provider is called, because refusing after the call would report a refusal for work
|
|
35
|
+
* that has already been billed (HLM-09).
|
|
36
|
+
*
|
|
37
|
+
* @module @try-works/tool-mode-recipes/broker
|
|
38
|
+
*/
|
|
39
|
+
import { errorCodeOf, INITIAL_MAX_IMAGE_INPUTS, INITIAL_MAX_OUTPUT_TOKENS, renderPrompt, validateRecipeInputs, } from "./recipe-format.js";
|
|
40
|
+
/**
|
|
41
|
+
* The initial policy: RES-10's initial per-leaf maxima, with recording required.
|
|
42
|
+
*
|
|
43
|
+
* Recording defaults to REQUIRED because HLM-09 makes reconstructability a DSH
|
|
44
|
+
* requirement, and a default of "off" is how a harness call ends up unrecorded without
|
|
45
|
+
* anyone deciding that.
|
|
46
|
+
*/
|
|
47
|
+
export const INITIAL_HOST_POLICY = {
|
|
48
|
+
requireRecording: true,
|
|
49
|
+
remoteDestinations: true,
|
|
50
|
+
outputTokens: INITIAL_MAX_OUTPUT_TOKENS,
|
|
51
|
+
imageInputs: INITIAL_MAX_IMAGE_INPUTS,
|
|
52
|
+
};
|
|
53
|
+
/** A local-only policy, which forbids remote destinations however the tool was built. */
|
|
54
|
+
export const LOCAL_ONLY_HOST_POLICY = {
|
|
55
|
+
...INITIAL_HOST_POLICY,
|
|
56
|
+
remoteDestinations: false,
|
|
57
|
+
};
|
|
58
|
+
/**
|
|
59
|
+
* Whether a policy permits reaching a destination (RES-10).
|
|
60
|
+
*
|
|
61
|
+
* Mirrored from the host's `harness-budgets.permitsDestination`: a local-only profile
|
|
62
|
+
* prohibits remote destinations INDEPENDENTLY of how the tool was implemented, so this
|
|
63
|
+
* asks the policy rather than the implementation. `local` is always permitted.
|
|
64
|
+
* @param policy - the policy in force.
|
|
65
|
+
* @param destination - the destination being considered.
|
|
66
|
+
* @returns whether it is permitted.
|
|
67
|
+
*/
|
|
68
|
+
export function permitsDestination(policy, destination) {
|
|
69
|
+
return destination === 'local' ? true : policy.remoteDestinations;
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Narrow a policy by a requested reduction (RES-10, HLM-07).
|
|
73
|
+
*
|
|
74
|
+
* Every figure takes the MINIMUM, so a request can only lower a limit; that direction is
|
|
75
|
+
* the point, since a caller asking for more than the profile permits must get the
|
|
76
|
+
* profile's figure. `requireRecording` is an OR: a request can demand recording where
|
|
77
|
+
* the base was silent, and can never switch it off.
|
|
78
|
+
* @param base - the policy in force.
|
|
79
|
+
* @param requested - what the caller asked for.
|
|
80
|
+
* @returns the effective policy.
|
|
81
|
+
*/
|
|
82
|
+
export function narrowBrokerPolicy(base, requested) {
|
|
83
|
+
const lower = (a, b) => (b === undefined ? a : Math.min(a, b));
|
|
84
|
+
return {
|
|
85
|
+
requireRecording: base.requireRecording || (requested.requireRecording ?? false),
|
|
86
|
+
// A reduction can never ENABLE remote destinations, so this is an AND.
|
|
87
|
+
remoteDestinations: base.remoteDestinations && (requested.remoteDestinations ?? true),
|
|
88
|
+
outputTokens: lower(base.outputTokens, requested.outputTokens),
|
|
89
|
+
imageInputs: lower(base.imageInputs, requested.imageInputs),
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
/** Raised by {@link HarnessBroker.runOrThrow}. */
|
|
93
|
+
export class BrokerError extends Error {
|
|
94
|
+
name = 'BrokerError';
|
|
95
|
+
/** The failure code. */
|
|
96
|
+
code;
|
|
97
|
+
constructor(code, message) {
|
|
98
|
+
super(`${code}: ${message}`);
|
|
99
|
+
this.code = code;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
/** Normalise the two-state shorthand into the three-state capability. */
|
|
103
|
+
function imageCapability(value) {
|
|
104
|
+
if (value === true)
|
|
105
|
+
return 'supported';
|
|
106
|
+
if (value === false)
|
|
107
|
+
return 'unsupported';
|
|
108
|
+
return value;
|
|
109
|
+
}
|
|
110
|
+
/** Name a thrown value for a failure reason. */
|
|
111
|
+
function messageOf(error) {
|
|
112
|
+
try {
|
|
113
|
+
return error instanceof Error ? error.message : String(error);
|
|
114
|
+
}
|
|
115
|
+
catch {
|
|
116
|
+
return '<unrenderable>';
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
/** The codes a binding error may carry, most specific first in the report. */
|
|
120
|
+
const BINDING_CODES = ['RECIPE_BINDING_ERROR', 'MODEL_BUDGET_EXCEEDED', 'UNSUPPORTED_MODALITY'];
|
|
121
|
+
/**
|
|
122
|
+
* The code a binding failure reports.
|
|
123
|
+
*
|
|
124
|
+
* The individual errors carry their own codes, and collapsing a specific one into a
|
|
125
|
+
* blanket `RECIPE_BINDING_ERROR` would destroy the distinction RCP-11 exists to keep: a
|
|
126
|
+
* caller that cannot tell "you sent a PDF where an image belongs" from "you misspelled a
|
|
127
|
+
* field" cannot respond correctly to either. The first error is used, because that is the
|
|
128
|
+
* one the caller has to fix first.
|
|
129
|
+
* @param errors - the rendered binding errors.
|
|
130
|
+
* @returns the code to report.
|
|
131
|
+
*/
|
|
132
|
+
function bindingCode(errors) {
|
|
133
|
+
const code = errorCodeOf(errors[0] ?? '');
|
|
134
|
+
return BINDING_CODES.find(candidate => candidate === code) ?? 'RECIPE_BINDING_ERROR';
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* Build the ONE request a recipe can produce (HLM-05).
|
|
138
|
+
*
|
|
139
|
+
* Exported because the isolation property deserves to be checkable without a provider:
|
|
140
|
+
* the request object's key set IS the claim that nothing else is sent.
|
|
141
|
+
* @param recipe - the recipe being run.
|
|
142
|
+
* @param values - validated bindings.
|
|
143
|
+
* @param maxOutputTokens - the effective ceiling.
|
|
144
|
+
* @param signal - the cancellation channel.
|
|
145
|
+
* @returns the frozen request.
|
|
146
|
+
*/
|
|
147
|
+
export function buildIsolatedRequest(recipe, values, maxOutputTokens, signal) {
|
|
148
|
+
const images = imagesOf(recipe, values);
|
|
149
|
+
return Object.freeze({
|
|
150
|
+
prompt: renderPrompt(recipe, values),
|
|
151
|
+
...(images.length === 0
|
|
152
|
+
? {}
|
|
153
|
+
: { images: Object.freeze(images.map(image => Object.freeze({ mediaType: image.mediaType, bytes: image.bytes }))) }),
|
|
154
|
+
maxOutputTokens,
|
|
155
|
+
signal,
|
|
156
|
+
/*
|
|
157
|
+
* `system` is absent, and that is the assertion: a recipe in this format cannot
|
|
158
|
+
* supply a system prompt, and the parent's must not be inherited (HLM-05). A request
|
|
159
|
+
* with a system member would be one built from something other than the recipe.
|
|
160
|
+
*/
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
/** The bound images of a recipe, in declaration order. */
|
|
164
|
+
function imagesOf(recipe, values) {
|
|
165
|
+
const images = [];
|
|
166
|
+
for (const [name, declaration] of Object.entries(recipe.inputs)) {
|
|
167
|
+
if (declaration.type !== 'image')
|
|
168
|
+
continue;
|
|
169
|
+
const value = values[name];
|
|
170
|
+
if (value !== undefined)
|
|
171
|
+
images.push(value);
|
|
172
|
+
}
|
|
173
|
+
return images;
|
|
174
|
+
}
|
|
175
|
+
/**
|
|
176
|
+
* The harness broker.
|
|
177
|
+
*
|
|
178
|
+
* One instance holds the authorizations (which tool may use which provider generation)
|
|
179
|
+
* and the host policy. It is deliberately not a registry of tools: nothing here switches
|
|
180
|
+
* on a tool name, which is what RCP-12 forbids and what the tests assert by running two
|
|
181
|
+
* different recipes through the same code path.
|
|
182
|
+
*/
|
|
183
|
+
export class HarnessBroker {
|
|
184
|
+
#grants = new Map();
|
|
185
|
+
#policy;
|
|
186
|
+
#record;
|
|
187
|
+
constructor(options = {}) {
|
|
188
|
+
this.#policy = options.policy ?? INITIAL_HOST_POLICY;
|
|
189
|
+
this.#record = options.record;
|
|
190
|
+
/**
|
|
191
|
+
* Refuse to exist in a state where recording would be claimed and not performed.
|
|
192
|
+
*
|
|
193
|
+
* The alternative — accepting the policy and journaling nothing — is the failure
|
|
194
|
+
* mode HLM-09 names: a request that must be reconstructable and is not, with nothing
|
|
195
|
+
* in the pipeline looking wrong.
|
|
196
|
+
*/
|
|
197
|
+
if (this.#policy.requireRecording && this.#record === undefined) {
|
|
198
|
+
throw new Error('the host policy requires recording (HLM-09) but no journal sink was provided. Refusing to construct a broker '
|
|
199
|
+
+ 'that would report recorded requests it never recorded; pass a sink or set requireRecording to false');
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
/** The host policy in force. */
|
|
203
|
+
get policy() {
|
|
204
|
+
return this.#policy;
|
|
205
|
+
}
|
|
206
|
+
/**
|
|
207
|
+
* Authorize a provider generation for one tool (HLM-02).
|
|
208
|
+
*
|
|
209
|
+
* The generation is CAPTURED here rather than read at dispatch time, so a provider that
|
|
210
|
+
* reloads between authorization and dispatch stops being authorized instead of being
|
|
211
|
+
* silently used under the old decision.
|
|
212
|
+
* @param toolName - the tool this authorization covers.
|
|
213
|
+
* @param provider - the provider.
|
|
214
|
+
*/
|
|
215
|
+
authorize(toolName, provider) {
|
|
216
|
+
const grant = { toolName, provider, generation: provider.generation };
|
|
217
|
+
this.#grants.set(toolName, grant);
|
|
218
|
+
return grant;
|
|
219
|
+
}
|
|
220
|
+
/**
|
|
221
|
+
* Revoke a tool's authorization.
|
|
222
|
+
* @param toolName - the tool to revoke.
|
|
223
|
+
* @returns whether an authorization existed.
|
|
224
|
+
*/
|
|
225
|
+
revoke(toolName) {
|
|
226
|
+
return this.#grants.delete(toolName);
|
|
227
|
+
}
|
|
228
|
+
/**
|
|
229
|
+
* The live authorization for a tool, or `undefined`.
|
|
230
|
+
*
|
|
231
|
+
* `undefined` covers both "never authorized" and "authorized for a generation this
|
|
232
|
+
* provider no longer is", because from a caller's point of view both mean the same
|
|
233
|
+
* thing: this invocation has no authorized executor. {@link resolveGrant} keeps the
|
|
234
|
+
* two apart for the error message.
|
|
235
|
+
* @param toolName - the tool to look up.
|
|
236
|
+
* @returns the grant, when it is still valid.
|
|
237
|
+
*/
|
|
238
|
+
grantFor(toolName) {
|
|
239
|
+
const resolved = this.#resolveGrant(toolName);
|
|
240
|
+
return resolved.kind === 'ok' ? this.#grants.get(toolName) : undefined;
|
|
241
|
+
}
|
|
242
|
+
/** Resolve, keeping "missing" and "stale" distinct for the refusal reason. */
|
|
243
|
+
#resolveGrant(toolName) {
|
|
244
|
+
const grant = this.#grants.get(toolName);
|
|
245
|
+
if (grant === undefined)
|
|
246
|
+
return { kind: 'missing' };
|
|
247
|
+
const current = grant.provider.generation;
|
|
248
|
+
if (current !== grant.generation) {
|
|
249
|
+
return { kind: 'stale', authorized: grant.generation, current };
|
|
250
|
+
}
|
|
251
|
+
return { kind: 'ok', provider: grant.provider, generation: grant.generation };
|
|
252
|
+
}
|
|
253
|
+
/**
|
|
254
|
+
* Run one recipe invocation.
|
|
255
|
+
*
|
|
256
|
+
* Cancellation, policy refusals and provider failures all come back as outcomes rather
|
|
257
|
+
* than exceptions, so a caller that must report a result cannot lose one to an
|
|
258
|
+
* unhandled rejection. Use {@link runOrThrow} where an exception is the right shape.
|
|
259
|
+
* @param recipe - the recipe to run.
|
|
260
|
+
* @param args - the caller's arguments, validated against the recipe's inputs.
|
|
261
|
+
* @param options - the cancellation channel and deadline.
|
|
262
|
+
* @returns the outcome: never a success that carries no text.
|
|
263
|
+
*/
|
|
264
|
+
async run(recipe, args, options = {}) {
|
|
265
|
+
const controller = new AbortController();
|
|
266
|
+
let cancelReason = 'the caller aborted the invocation';
|
|
267
|
+
const caller = options.signal;
|
|
268
|
+
const onCallerAbort = () => {
|
|
269
|
+
cancelReason = 'the caller aborted the invocation';
|
|
270
|
+
controller.abort();
|
|
271
|
+
};
|
|
272
|
+
if (caller !== undefined) {
|
|
273
|
+
if (caller.aborted) {
|
|
274
|
+
cancelReason = 'the caller aborted the invocation before it started';
|
|
275
|
+
controller.abort();
|
|
276
|
+
}
|
|
277
|
+
else {
|
|
278
|
+
caller.addEventListener('abort', onCallerAbort, { once: true });
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
const deadlineMs = options.deadlineMs;
|
|
282
|
+
const timer = deadlineMs === undefined
|
|
283
|
+
? undefined
|
|
284
|
+
: setTimeout(() => {
|
|
285
|
+
cancelReason = `the invocation deadline of ${deadlineMs}ms expired`;
|
|
286
|
+
controller.abort();
|
|
287
|
+
}, deadlineMs);
|
|
288
|
+
try {
|
|
289
|
+
return await this.#invoke(recipe, args, controller.signal, () => cancelReason);
|
|
290
|
+
}
|
|
291
|
+
finally {
|
|
292
|
+
// Cleaning up here is not tidiness: a listener left on a long-lived signal is a
|
|
293
|
+
// leak per invocation, and a timer left running keeps the process alive.
|
|
294
|
+
if (timer !== undefined)
|
|
295
|
+
clearTimeout(timer);
|
|
296
|
+
if (caller !== undefined)
|
|
297
|
+
caller.removeEventListener('abort', onCallerAbort);
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
/**
|
|
301
|
+
* Run one recipe invocation, throwing on failure or cancellation.
|
|
302
|
+
* @param recipe - the recipe to run.
|
|
303
|
+
* @param args - the caller's arguments.
|
|
304
|
+
* @param options - the cancellation channel and deadline.
|
|
305
|
+
* @returns the completed outcome.
|
|
306
|
+
* @throws BrokerError on any failure, including cancellation.
|
|
307
|
+
*/
|
|
308
|
+
async runOrThrow(recipe, args, options = {}) {
|
|
309
|
+
const outcome = await this.run(recipe, args, options);
|
|
310
|
+
if (outcome.kind === 'completed')
|
|
311
|
+
return outcome;
|
|
312
|
+
if (outcome.kind === 'cancelled')
|
|
313
|
+
throw new BrokerError('CANCELLED', outcome.reason);
|
|
314
|
+
throw new BrokerError(outcome.code, outcome.reason);
|
|
315
|
+
}
|
|
316
|
+
/** The invocation itself: checks in the order they must happen, then one dispatch. */
|
|
317
|
+
async #invoke(recipe, args, signal, reason) {
|
|
318
|
+
// Cancellation first: a cancelled call must not do any of the work below, and the
|
|
319
|
+
// provider must not be called at all.
|
|
320
|
+
if (signal.aborted)
|
|
321
|
+
return { kind: 'cancelled', reason: reason() };
|
|
322
|
+
const binding = validateRecipeInputs(recipe, args);
|
|
323
|
+
if (!binding.ok) {
|
|
324
|
+
return { kind: 'failed', code: bindingCode(binding.errors), reason: binding.errors.join('; ') };
|
|
325
|
+
}
|
|
326
|
+
const resolved = this.#resolveGrant(recipe.tool.name);
|
|
327
|
+
if (resolved.kind === 'missing') {
|
|
328
|
+
return {
|
|
329
|
+
kind: 'failed',
|
|
330
|
+
code: 'EXECUTOR_NOT_CONFIGURED',
|
|
331
|
+
reason: `no executor is authorized for ${recipe.tool.name}. A recipe is a request for a host-approved harness `
|
|
332
|
+
+ 'model, not an authorization to reach one (RCP-05)',
|
|
333
|
+
};
|
|
334
|
+
}
|
|
335
|
+
if (resolved.kind === 'stale') {
|
|
336
|
+
return {
|
|
337
|
+
kind: 'failed',
|
|
338
|
+
code: 'EXECUTOR_NOT_CONFIGURED',
|
|
339
|
+
reason: `the authorization for ${recipe.tool.name} covers provider generation ${resolved.authorized}, but the `
|
|
340
|
+
+ `provider now reports generation ${resolved.current}. An authorization is for a generation, not for a `
|
|
341
|
+
+ 'name: re-authorize it rather than running new endpoint code under the old decision (HLM-02)',
|
|
342
|
+
};
|
|
343
|
+
}
|
|
344
|
+
const provider = resolved.provider;
|
|
345
|
+
/**
|
|
346
|
+
* ONE capability snapshot per invocation.
|
|
347
|
+
*
|
|
348
|
+
* Reading capabilities again after the call, or per check, is how one model's facts
|
|
349
|
+
* end up combined with another endpoint's after a concurrent switch (HLM-02).
|
|
350
|
+
*/
|
|
351
|
+
const capabilities = provider.capabilities();
|
|
352
|
+
const values = binding.values;
|
|
353
|
+
const images = imagesOf(recipe, values);
|
|
354
|
+
if (images.length > 0) {
|
|
355
|
+
const imagesCapability = imageCapability(capabilities.images);
|
|
356
|
+
if (imagesCapability === 'unsupported') {
|
|
357
|
+
return {
|
|
358
|
+
kind: 'failed',
|
|
359
|
+
code: 'IMAGE_INPUT_UNAVAILABLE',
|
|
360
|
+
reason: `${recipe.tool.name} binds ${images.length} image input(s) but provider "${provider.id}" reports no `
|
|
361
|
+
+ 'image support. Refusing rather than sending the text alone: a model asked to describe an image it never '
|
|
362
|
+
+ 'received produces a fluent invention, and that is worse than an error (HLM-11)',
|
|
363
|
+
};
|
|
364
|
+
}
|
|
365
|
+
if (imagesCapability === 'unknown') {
|
|
366
|
+
return {
|
|
367
|
+
kind: 'failed',
|
|
368
|
+
code: 'EXECUTOR_CAPABILITY_UNKNOWN',
|
|
369
|
+
reason: `provider "${provider.id}" has not reported whether it accepts images, and ${recipe.tool.name} binds `
|
|
370
|
+
+ 'an image input. An unevaluated capability is not a capability: refusing rather than assuming it '
|
|
371
|
+
+ '(MODEL-06)',
|
|
372
|
+
};
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
/**
|
|
376
|
+
* The image ceiling is the INTERSECTION of every limit (HLM-07, RES-10).
|
|
377
|
+
*
|
|
378
|
+
* A narrowed host policy must be able to refuse images a recipe permits, which is
|
|
379
|
+
* the only direction a limit may move.
|
|
380
|
+
*/
|
|
381
|
+
const imageCeiling = Math.min(recipe.imageInputs.max, this.#policy.imageInputs, INITIAL_MAX_IMAGE_INPUTS);
|
|
382
|
+
if (images.length > imageCeiling) {
|
|
383
|
+
return {
|
|
384
|
+
kind: 'failed',
|
|
385
|
+
code: 'MODEL_BUDGET_EXCEEDED',
|
|
386
|
+
reason: `${images.length} image input(s) supplied against an effective ceiling of ${imageCeiling} (recipe `
|
|
387
|
+
+ `${recipe.imageInputs.max}, host ${this.#policy.imageInputs}, RES-10 ${INITIAL_MAX_IMAGE_INPUTS})`,
|
|
388
|
+
};
|
|
389
|
+
}
|
|
390
|
+
if (!permitsDestination(this.#policy, provider.destination)) {
|
|
391
|
+
return {
|
|
392
|
+
kind: 'failed',
|
|
393
|
+
code: 'EXECUTOR_NOT_CONFIGURED',
|
|
394
|
+
reason: `provider "${provider.id}" is a ${provider.destination} destination and the policy in force permits `
|
|
395
|
+
+ 'local destinations only. A local-only profile prohibits remote destinations however the tool was built, '
|
|
396
|
+
+ 'so this is refused rather than routed (RES-10)',
|
|
397
|
+
};
|
|
398
|
+
}
|
|
399
|
+
const maxOutputTokens = Math.min(recipe.maxOutputTokens, this.#policy.outputTokens, capabilities.maxOutputTokens);
|
|
400
|
+
if (!Number.isInteger(maxOutputTokens) || maxOutputTokens < 1) {
|
|
401
|
+
return {
|
|
402
|
+
kind: 'failed',
|
|
403
|
+
code: 'MODEL_BUDGET_EXCEEDED',
|
|
404
|
+
reason: `the effective output-token ceiling is ${maxOutputTokens} (recipe ${recipe.maxOutputTokens}, host `
|
|
405
|
+
+ `${this.#policy.outputTokens}, provider ${capabilities.maxOutputTokens}). A call permitted to produce `
|
|
406
|
+
+ 'nothing cannot produce a result (RES-10)',
|
|
407
|
+
};
|
|
408
|
+
}
|
|
409
|
+
/**
|
|
410
|
+
* Recording is settled BEFORE dispatch.
|
|
411
|
+
*
|
|
412
|
+
* HLM-09: if mandatory recording conflicts with policy, refuse rather than silently
|
|
413
|
+
* drop it. Refusing here means no provider is called, so a refused invocation is not
|
|
414
|
+
* a billed one.
|
|
415
|
+
*/
|
|
416
|
+
if (!recipe.policy.record && this.#policy.requireRecording) {
|
|
417
|
+
return {
|
|
418
|
+
kind: 'failed',
|
|
419
|
+
code: 'CAPTURE_POLICY_CONFLICT',
|
|
420
|
+
reason: `${recipe.tool.name} declares record: false while the host requires the prepared request to be `
|
|
421
|
+
+ 'journaled. Refusing rather than running unrecorded: a request that must be reconstructable and is not is '
|
|
422
|
+
+ 'the failure HLM-09 names, and it leaves nothing in the pipeline looking wrong',
|
|
423
|
+
};
|
|
424
|
+
}
|
|
425
|
+
let recorded = false;
|
|
426
|
+
/**
|
|
427
|
+
* The request is built BEFORE the journal entry, and the entry quotes THAT request.
|
|
428
|
+
*
|
|
429
|
+
* HLM-09 asks the journal to record the exact prepared request. Rendering the prompt
|
|
430
|
+
* separately for the entry would make the record a second rendering that merely ought
|
|
431
|
+
* to agree with the one that was sent; quoting the request's own prompt makes the two
|
|
432
|
+
* the same string by construction.
|
|
433
|
+
*/
|
|
434
|
+
const request = buildIsolatedRequest(recipe, values, maxOutputTokens, signal);
|
|
435
|
+
if (recipe.policy.record) {
|
|
436
|
+
const sink = this.#record;
|
|
437
|
+
if (sink === undefined) {
|
|
438
|
+
return {
|
|
439
|
+
kind: 'failed',
|
|
440
|
+
code: 'CAPTURE_POLICY_CONFLICT',
|
|
441
|
+
reason: 'recording is required but no journal sink is configured, so the prepared request would be lost',
|
|
442
|
+
};
|
|
443
|
+
}
|
|
444
|
+
const entry = {
|
|
445
|
+
kind: 'harness-model-auxiliary-call',
|
|
446
|
+
toolName: recipe.tool.name,
|
|
447
|
+
contractId: recipe.tool.contractId,
|
|
448
|
+
recipeId: recipe.id,
|
|
449
|
+
recipeVersion: recipe.version,
|
|
450
|
+
providerId: provider.id,
|
|
451
|
+
providerGeneration: resolved.generation,
|
|
452
|
+
destination: provider.destination,
|
|
453
|
+
maxOutputTokens,
|
|
454
|
+
imageCount: images.length,
|
|
455
|
+
prompt: request.prompt,
|
|
456
|
+
context: 'recipe-only',
|
|
457
|
+
};
|
|
458
|
+
try {
|
|
459
|
+
await sink(entry);
|
|
460
|
+
recorded = true;
|
|
461
|
+
}
|
|
462
|
+
catch (error) {
|
|
463
|
+
return {
|
|
464
|
+
kind: 'failed',
|
|
465
|
+
code: 'RECORDING_FAILED',
|
|
466
|
+
reason: `the auxiliary-call journal rejected the entry (${messageOf(error)}), so the call did not run. A call `
|
|
467
|
+
+ 'whose record cannot be written is not one this broker will make',
|
|
468
|
+
};
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
const dispatched = await dispatch(provider, request, signal, reason);
|
|
472
|
+
if (dispatched.kind === 'cancelled')
|
|
473
|
+
return { kind: 'cancelled', reason: dispatched.reason };
|
|
474
|
+
if (dispatched.kind === 'failed') {
|
|
475
|
+
return { kind: 'failed', code: 'PROVIDER_FAILED', reason: dispatched.reason };
|
|
476
|
+
}
|
|
477
|
+
/**
|
|
478
|
+
* A signal aborted while the provider was working is a cancellation, whatever came
|
|
479
|
+
* back.
|
|
480
|
+
*
|
|
481
|
+
* Checking after the await is the difference between "we stopped asking" and "we
|
|
482
|
+
* stopped caring": the caller cancelled, so a result that arrived afterwards is not
|
|
483
|
+
* one it asked for, and reporting it as a success would attach an answer to a
|
|
484
|
+
* cancelled invocation (HLM-10).
|
|
485
|
+
*/
|
|
486
|
+
if (signal.aborted) {
|
|
487
|
+
return { kind: 'cancelled', reason: `${reason()}; the provider's result was discarded rather than reported` };
|
|
488
|
+
}
|
|
489
|
+
/**
|
|
490
|
+
* The generation is rechecked when the call returns, and a change makes the result
|
|
491
|
+
* UNATTRIBUTABLE.
|
|
492
|
+
*
|
|
493
|
+
* HLM-02 forbids combining one model's capability facts with another endpoint, and a
|
|
494
|
+
* provider that reloads while the call is in flight leaves the broker holding a result
|
|
495
|
+
* it cannot honestly attribute to either generation. Reporting it under the resolved
|
|
496
|
+
* generation would be a receipt claiming provenance the broker does not have.
|
|
497
|
+
*/
|
|
498
|
+
if (provider.generation !== resolved.generation) {
|
|
499
|
+
return {
|
|
500
|
+
kind: 'failed',
|
|
501
|
+
code: 'PROVIDER_FAILED',
|
|
502
|
+
reason: `provider "${provider.id}" changed generation from ${resolved.generation} to ${provider.generation} `
|
|
503
|
+
+ 'while the call was in flight, so the result cannot be attributed to the endpoint this invocation '
|
|
504
|
+
+ 'resolved. Refusing rather than reporting it under either generation (HLM-02)',
|
|
505
|
+
};
|
|
506
|
+
}
|
|
507
|
+
const completion = dispatched.completion;
|
|
508
|
+
const identity = {
|
|
509
|
+
provider: { id: provider.id, generation: resolved.generation, destination: provider.destination },
|
|
510
|
+
recipe: {
|
|
511
|
+
id: recipe.id,
|
|
512
|
+
version: recipe.version,
|
|
513
|
+
toolName: recipe.tool.name,
|
|
514
|
+
contractId: recipe.tool.contractId,
|
|
515
|
+
},
|
|
516
|
+
};
|
|
517
|
+
switch (completion.kind) {
|
|
518
|
+
case 'refused':
|
|
519
|
+
return {
|
|
520
|
+
kind: 'failed',
|
|
521
|
+
code: 'MODEL_REFUSED',
|
|
522
|
+
reason: `the model declined: ${completion.reason}. A refusal is an error and never an empty result, because a `
|
|
523
|
+
+ 'caller writing an empty result into a document sees a blank field rather than a failure',
|
|
524
|
+
};
|
|
525
|
+
case 'truncated':
|
|
526
|
+
return {
|
|
527
|
+
kind: 'failed',
|
|
528
|
+
code: 'MODEL_OUTPUT_TRUNCATED',
|
|
529
|
+
reason: 'the generation was cut short, so no complete result was produced. Partial prose is not a successful '
|
|
530
|
+
+ 'result (HLM-07)',
|
|
531
|
+
};
|
|
532
|
+
case 'tool_call':
|
|
533
|
+
return {
|
|
534
|
+
kind: 'failed',
|
|
535
|
+
code: 'UNEXPECTED_MODEL_TOOL_CALL',
|
|
536
|
+
reason: `the provider emitted a tool call for "${completion.toolName}". No tool catalog was supplied, so a `
|
|
537
|
+
+ 'tool call is not executed and no agent loop continues from it (HLM-06)',
|
|
538
|
+
};
|
|
539
|
+
case 'text': {
|
|
540
|
+
if (completion.text.trim().length === 0) {
|
|
541
|
+
return {
|
|
542
|
+
kind: 'failed',
|
|
543
|
+
code: 'INVALID_MODEL_OUTPUT',
|
|
544
|
+
reason: 'the provider returned empty text. An empty completion is not a result: reporting it as one '
|
|
545
|
+
+ 'produces an empty title, an empty description and no error anywhere',
|
|
546
|
+
};
|
|
547
|
+
}
|
|
548
|
+
return {
|
|
549
|
+
kind: 'completed',
|
|
550
|
+
text: completion.text,
|
|
551
|
+
usage: completion.usage ?? {
|
|
552
|
+
known: false,
|
|
553
|
+
reason: 'the provider reported no usage for this call',
|
|
554
|
+
},
|
|
555
|
+
recorded,
|
|
556
|
+
...identity,
|
|
557
|
+
};
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
/**
|
|
563
|
+
* Dispatch one call, honouring cancellation and turning a provider throw into a failure.
|
|
564
|
+
* @param provider - the provider.
|
|
565
|
+
* @param request - the isolated request.
|
|
566
|
+
* @param signal - the cancellation channel.
|
|
567
|
+
* @param reason - reads the current cancellation reason.
|
|
568
|
+
* @returns how the dispatch settled.
|
|
569
|
+
*/
|
|
570
|
+
async function dispatch(provider, request, signal, reason) {
|
|
571
|
+
if (signal.aborted)
|
|
572
|
+
return { kind: 'cancelled', reason: reason() };
|
|
573
|
+
let onAbort;
|
|
574
|
+
const cancelled = new Promise((resolve) => {
|
|
575
|
+
onAbort = () => resolve({ kind: 'cancelled', reason: reason() });
|
|
576
|
+
signal.addEventListener('abort', onAbort, { once: true });
|
|
577
|
+
});
|
|
578
|
+
try {
|
|
579
|
+
return await Promise.race([
|
|
580
|
+
provider.complete(request).then((completion) => ({ kind: 'completion', completion }),
|
|
581
|
+
// The provider's own rejection is handled here rather than escaping as an
|
|
582
|
+
// unhandled rejection after a cancellation already won the race.
|
|
583
|
+
(error) => ({ kind: 'failed', reason: `provider "${provider.id}" failed: ${messageOf(error)}` })),
|
|
584
|
+
cancelled,
|
|
585
|
+
]);
|
|
586
|
+
}
|
|
587
|
+
finally {
|
|
588
|
+
if (onAbort !== undefined)
|
|
589
|
+
signal.removeEventListener('abort', onAbort);
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
//# sourceMappingURL=broker.js.map
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The recipe authoring CLI (RCP-08).
|
|
3
|
+
*
|
|
4
|
+
* Three commands, each a required new tool rather than an existing npm/DSH
|
|
5
|
+
* command: `validate` checks a draft against BOTH authorities (the published
|
|
6
|
+
* `harness-model-recipe.schema.json` shipped beside this module AND the code
|
|
7
|
+
* validator `parseRecipe`), `describe` prints the effective bounds an operator
|
|
8
|
+
* reads before deciding, and `pack` builds a declarative-extension package
|
|
9
|
+
* (manifest + recipes, NO executable code) consumable by the extension install
|
|
10
|
+
* path.
|
|
11
|
+
*
|
|
12
|
+
* Guardrails built in rather than documented:
|
|
13
|
+
*
|
|
14
|
+
* - The schema is read from THIS PACKAGE'S SHIPPED FILE, resolved relative to
|
|
15
|
+
* the module (so a packed install validates against the packed artifact, not
|
|
16
|
+
* some source checkout).
|
|
17
|
+
* - Packing validates the exact production artifact and runs NO lifecycle
|
|
18
|
+
* scripts: the output is written directly (directory + a store-method zip),
|
|
19
|
+
* never through `npm pack`.
|
|
20
|
+
* - Nothing here runs inference, installs or activates anything, or touches
|
|
21
|
+
* the network (RCP-08's creation rules).
|
|
22
|
+
* - Exit codes are a contract: 0 valid, 1 invalid (every refusal named),
|
|
23
|
+
* 2 usage or IO failure.
|
|
24
|
+
*
|
|
25
|
+
* RCP-09 (agent-assisted creation with an authoring grant) is NOT claimed by
|
|
26
|
+
* this module: the CLI is a plain argv/stdio tool, which an agent CAN drive,
|
|
27
|
+
* but the authoring-grant and draft-directory policy belong to the host and are
|
|
28
|
+
* left as the recorded remainder.
|
|
29
|
+
*
|
|
30
|
+
* @module @try-works/tool-mode-recipes/cli
|
|
31
|
+
*/
|
|
32
|
+
import { type RecipeManifest } from './recipe-format.ts';
|
|
33
|
+
/** Where the CLI writes. Injected so a test can capture output. */
|
|
34
|
+
export interface RecipeCliIo {
|
|
35
|
+
out(line: string): void;
|
|
36
|
+
err(line: string): void;
|
|
37
|
+
}
|
|
38
|
+
/** The two-verdict validation of one recipe document. */
|
|
39
|
+
export interface RecipeVerdict {
|
|
40
|
+
/** The published-schema verdict. */
|
|
41
|
+
readonly schemaOk: boolean;
|
|
42
|
+
/** Rendered schema violations, one per refusal. */
|
|
43
|
+
readonly schemaViolations: readonly string[];
|
|
44
|
+
/** The code-validator verdict. */
|
|
45
|
+
readonly codeOk: boolean;
|
|
46
|
+
/** The code validator's rendered refusals. */
|
|
47
|
+
readonly codeViolations: readonly string[];
|
|
48
|
+
/** The parsed recipe, when the code validator admits the document. */
|
|
49
|
+
readonly recipe?: RecipeManifest;
|
|
50
|
+
}
|
|
51
|
+
/** Validate one decoded document against BOTH authorities (RCP-02, RCP-08). */
|
|
52
|
+
export declare function judgeRecipe(document: unknown): RecipeVerdict;
|
|
53
|
+
/**
|
|
54
|
+
* Run one CLI invocation.
|
|
55
|
+
* @param argv - arguments after the executable name.
|
|
56
|
+
* @param io - the output sinks.
|
|
57
|
+
* @returns a promise of the process exit code.
|
|
58
|
+
*/
|
|
59
|
+
export declare function runRecipeCli(argv: readonly string[], io: RecipeCliIo): Promise<number>;
|
|
60
|
+
//# sourceMappingURL=cli.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AAoBH,OAAO,EAKL,KAAK,cAAc,EACpB,MAAM,oBAAoB,CAAA;AAe3B,mEAAmE;AACnE,MAAM,WAAW,WAAW;IAC1B,GAAG,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAAA;IACvB,GAAG,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAAA;CACxB;AA8BD,yDAAyD;AACzD,MAAM,WAAW,aAAa;IAC5B,oCAAoC;IACpC,QAAQ,CAAC,QAAQ,EAAE,OAAO,CAAA;IAC1B,mDAAmD;IACnD,QAAQ,CAAC,gBAAgB,EAAE,SAAS,MAAM,EAAE,CAAA;IAC5C,kCAAkC;IAClC,QAAQ,CAAC,MAAM,EAAE,OAAO,CAAA;IACxB,8CAA8C;IAC9C,QAAQ,CAAC,cAAc,EAAE,SAAS,MAAM,EAAE,CAAA;IAC1C,sEAAsE;IACtE,QAAQ,CAAC,MAAM,CAAC,EAAE,cAAc,CAAA;CACjC;AAED,+EAA+E;AAC/E,wBAAgB,WAAW,CAAC,QAAQ,EAAE,OAAO,GAAG,aAAa,CAW5D;AAyED;;;;;GAKG;AACH,wBAAsB,YAAY,CAAC,IAAI,EAAE,SAAS,MAAM,EAAE,EAAE,EAAE,EAAE,WAAW,GAAG,OAAO,CAAC,MAAM,CAAC,CAS5F"}
|