From f4f875d8512be64024ec149398d2f5708b145f1c Mon Sep 17 00:00:00 2001 From: Drew Stone Date: Sun, 21 Jun 2026 18:07:12 -0600 Subject: [PATCH 1/2] feat(delegate): generic delegate() over supervise() + delegate MCP tool MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add delegate(intent, opts) — the one generic delegation verb, a thin front door over supervise(). It hands the INTENT to a default authoring supervisor (router-brained, harness null, systemPrompt = supervisorInstructions()) which AUTHORS and spawns whatever worker the intent needs over the conserved-budget pool. No hardcoded coder/researcher profile; the supervisor writes its own. Returns supervise()'s SupervisedResult unchanged, so spentTotal (iterations, tokens, usd, ms) rides straight back — the cost channel delegate_code lacks. Add the delegate MCP tool (createDelegateHandler) as the agent-facing generic replacement for delegate_code/delegate_research: it routes to delegate(), takes the supervisor substrate (router/backend/deliverable) injected at server construction (like coderDelegate), and returns the delivered output WITH its spentTotal synchronously. Purely additive — delegate_code/delegate_research/composeProductionAgentProfile/ detachedSessionDelegate/coderProfile are all untouched. Reuses supervise(), the authoring skill, the conserved-budget pool, DeliverableSpec; no hand-rolled driver, spawn loop, or equal-k. Exports: delegate/DelegateOptions/defaultDelegateBudget from runtime barrel (/loops, next to supervise); the delegate tool + types from the mcp barrel. Regenerated docs/api/ for the new symbols. --- docs/api/index.md | 2 +- docs/api/mcp.md | 316 ++++++++++++++++++++++++++---- docs/api/runtime.md | 169 ++++++++++++++-- src/mcp/index.ts | 20 +- src/mcp/server.ts | 22 +++ src/mcp/tools/delegate.ts | 156 +++++++++++++++ src/runtime/index.ts | 9 + src/runtime/supervise/delegate.ts | 111 +++++++++++ tests/loops/delegate.test.ts | 156 +++++++++++++++ tests/mcp/delegate.test.ts | 116 +++++++++++ 10 files changed, 1019 insertions(+), 58 deletions(-) create mode 100644 src/mcp/tools/delegate.ts create mode 100644 src/runtime/supervise/delegate.ts create mode 100644 tests/loops/delegate.test.ts create mode 100644 tests/mcp/delegate.test.ts diff --git a/docs/api/index.md b/docs/api/index.md index 9e0fee47..8e422a1d 100644 --- a/docs/api/index.md +++ b/docs/api/index.md @@ -3172,7 +3172,7 @@ The supervisor-authored harness profiles — one fanout item (one worktree-CLI l ##### budget -> **budget**: [`Budget`](runtime.md#budget-9) +> **budget**: [`Budget`](runtime.md#budget-10) Defined in: [loop-runner.ts:166](https://github.com/tangle-network/agent-runtime/blob/main/src/loop-runner.ts#L166) diff --git a/docs/api/mcp.md b/docs/api/mcp.md index 32efd7b7..a9cfdbbe 100644 --- a/docs/api/mcp.md +++ b/docs/api/mcp.md @@ -2826,17 +2826,30 @@ Set when timeoutMs elapsed before exit. ### McpServerOptions -Defined in: [mcp/server.ts:64](https://github.com/tangle-network/agent-runtime/blob/main/src/mcp/server.ts#L64) +Defined in: [mcp/server.ts:71](https://github.com/tangle-network/agent-runtime/blob/main/src/mcp/server.ts#L71) **`Experimental`** #### Properties +##### delegateSupervisor? + +> `optional` **delegateSupervisor?**: [`DelegateHandlerOptions`](#delegatehandleroptions) + +Defined in: [mcp/server.ts:78](https://github.com/tangle-network/agent-runtime/blob/main/src/mcp/server.ts#L78) + +**`Experimental`** + +Required to enable `delegate` — the ONE generic delegation verb (the replacement for +delegate_code / delegate_research). Inject the supervisor substrate: its brain `router`, the +worker `backend`, and the completion `deliverable`. The supervisor AUTHORS its own worker from +the agent's intent, so there is no worker profile to wire here. + ##### coderDelegate? > `optional` **coderDelegate?**: [`CoderDelegate`](#coderdelegate) -Defined in: [mcp/server.ts:66](https://github.com/tangle-network/agent-runtime/blob/main/src/mcp/server.ts#L66) +Defined in: [mcp/server.ts:80](https://github.com/tangle-network/agent-runtime/blob/main/src/mcp/server.ts#L80) **`Experimental`** @@ -2846,7 +2859,7 @@ Required to enable delegate_code. > `optional` **researcherDelegate?**: [`ResearcherDelegate`](#researcherdelegate) -Defined in: [mcp/server.ts:73](https://github.com/tangle-network/agent-runtime/blob/main/src/mcp/server.ts#L73) +Defined in: [mcp/server.ts:87](https://github.com/tangle-network/agent-runtime/blob/main/src/mcp/server.ts#L87) **`Experimental`** @@ -2859,7 +2872,7 @@ researcher profile (typically `@tangle-network/agent-knowledge`'s > `optional` **uiAuditorDelegate?**: [`UiAuditorDelegate`](#uiauditordelegate) -Defined in: [mcp/server.ts:80](https://github.com/tangle-network/agent-runtime/blob/main/src/mcp/server.ts#L80) +Defined in: [mcp/server.ts:94](https://github.com/tangle-network/agent-runtime/blob/main/src/mcp/server.ts#L94) **`Experimental`** @@ -2872,7 +2885,7 @@ canonical in-process choice is `createInProcessUiAuditClient` from > `optional` **feedbackStore?**: [`FeedbackStore`](#feedbackstore) -Defined in: [mcp/server.ts:82](https://github.com/tangle-network/agent-runtime/blob/main/src/mcp/server.ts#L82) +Defined in: [mcp/server.ts:96](https://github.com/tangle-network/agent-runtime/blob/main/src/mcp/server.ts#L96) **`Experimental`** @@ -2882,7 +2895,7 @@ Override the default in-memory feedback store. > `optional` **queue?**: [`DelegationTaskQueue`](#delegationtaskqueue) -Defined in: [mcp/server.ts:84](https://github.com/tangle-network/agent-runtime/blob/main/src/mcp/server.ts#L84) +Defined in: [mcp/server.ts:98](https://github.com/tangle-network/agent-runtime/blob/main/src/mcp/server.ts#L98) **`Experimental`** @@ -2892,7 +2905,7 @@ Override the default in-memory task queue. > `optional` **detachedDispatch?**: `boolean` -Defined in: [mcp/server.ts:93](https://github.com/tangle-network/agent-runtime/blob/main/src/mcp/server.ts#L93) +Defined in: [mcp/server.ts:107](https://github.com/tangle-network/agent-runtime/blob/main/src/mcp/server.ts#L107) **`Experimental`** @@ -2907,7 +2920,7 @@ session-backed (sibling/fleet) placements. > `optional` **extraTools?**: [`McpToolDescriptor`](#mcptooldescriptor)[] -Defined in: [mcp/server.ts:99](https://github.com/tangle-network/agent-runtime/blob/main/src/mcp/server.ts#L99) +Defined in: [mcp/server.ts:113](https://github.com/tangle-network/agent-runtime/blob/main/src/mcp/server.ts#L113) **`Experimental`** @@ -2919,7 +2932,7 @@ duplicate name throws so delegation tools cannot be shadowed silently. > `optional` **traceContext?**: [`TraceContext`](#tracecontext-2) -Defined in: [mcp/server.ts:105](https://github.com/tangle-network/agent-runtime/blob/main/src/mcp/server.ts#L105) +Defined in: [mcp/server.ts:119](https://github.com/tangle-network/agent-runtime/blob/main/src/mcp/server.ts#L119) **`Experimental`** @@ -2931,7 +2944,7 @@ pass `traceContext` to that queue's constructor instead. > `optional` **serverName?**: `string` -Defined in: [mcp/server.ts:107](https://github.com/tangle-network/agent-runtime/blob/main/src/mcp/server.ts#L107) +Defined in: [mcp/server.ts:121](https://github.com/tangle-network/agent-runtime/blob/main/src/mcp/server.ts#L121) **`Experimental`** @@ -2941,7 +2954,7 @@ Server display name surfaced via `initialize`. Default `'agent-runtime-mcp'`. > `optional` **serverVersion?**: `string` -Defined in: [mcp/server.ts:109](https://github.com/tangle-network/agent-runtime/blob/main/src/mcp/server.ts#L109) +Defined in: [mcp/server.ts:123](https://github.com/tangle-network/agent-runtime/blob/main/src/mcp/server.ts#L123) **`Experimental`** @@ -2951,7 +2964,7 @@ Server version surfaced via `initialize`. Default = the package version baked at ### McpToolDescriptor -Defined in: [mcp/server.ts:113](https://github.com/tangle-network/agent-runtime/blob/main/src/mcp/server.ts#L113) +Defined in: [mcp/server.ts:127](https://github.com/tangle-network/agent-runtime/blob/main/src/mcp/server.ts#L127) **`Experimental`** @@ -2961,7 +2974,7 @@ Defined in: [mcp/server.ts:113](https://github.com/tangle-network/agent-runtime/ > **name**: `string` -Defined in: [mcp/server.ts:114](https://github.com/tangle-network/agent-runtime/blob/main/src/mcp/server.ts#L114) +Defined in: [mcp/server.ts:128](https://github.com/tangle-network/agent-runtime/blob/main/src/mcp/server.ts#L128) **`Experimental`** @@ -2969,7 +2982,7 @@ Defined in: [mcp/server.ts:114](https://github.com/tangle-network/agent-runtime/ > **description**: `string` -Defined in: [mcp/server.ts:115](https://github.com/tangle-network/agent-runtime/blob/main/src/mcp/server.ts#L115) +Defined in: [mcp/server.ts:129](https://github.com/tangle-network/agent-runtime/blob/main/src/mcp/server.ts#L129) **`Experimental`** @@ -2977,7 +2990,7 @@ Defined in: [mcp/server.ts:115](https://github.com/tangle-network/agent-runtime/ > **inputSchema**: `Record`\<`string`, `unknown`\> -Defined in: [mcp/server.ts:116](https://github.com/tangle-network/agent-runtime/blob/main/src/mcp/server.ts#L116) +Defined in: [mcp/server.ts:130](https://github.com/tangle-network/agent-runtime/blob/main/src/mcp/server.ts#L130) **`Experimental`** @@ -2985,7 +2998,7 @@ Defined in: [mcp/server.ts:116](https://github.com/tangle-network/agent-runtime/ > **handler**: (`raw`) => `Promise`\<`unknown`\> -Defined in: [mcp/server.ts:117](https://github.com/tangle-network/agent-runtime/blob/main/src/mcp/server.ts#L117) +Defined in: [mcp/server.ts:131](https://github.com/tangle-network/agent-runtime/blob/main/src/mcp/server.ts#L131) **`Experimental`** @@ -3003,7 +3016,7 @@ Defined in: [mcp/server.ts:117](https://github.com/tangle-network/agent-runtime/ ### McpServer -Defined in: [mcp/server.ts:121](https://github.com/tangle-network/agent-runtime/blob/main/src/mcp/server.ts#L121) +Defined in: [mcp/server.ts:135](https://github.com/tangle-network/agent-runtime/blob/main/src/mcp/server.ts#L135) **`Experimental`** @@ -3013,7 +3026,7 @@ Defined in: [mcp/server.ts:121](https://github.com/tangle-network/agent-runtime/ > `readonly` **tools**: `ReadonlyMap`\<`string`, [`McpToolDescriptor`](#mcptooldescriptor)\> -Defined in: [mcp/server.ts:123](https://github.com/tangle-network/agent-runtime/blob/main/src/mcp/server.ts#L123) +Defined in: [mcp/server.ts:137](https://github.com/tangle-network/agent-runtime/blob/main/src/mcp/server.ts#L137) **`Experimental`** @@ -3023,7 +3036,7 @@ Tools currently registered (depend on which delegates were wired). > `readonly` **queue**: [`DelegationTaskQueue`](#delegationtaskqueue) -Defined in: [mcp/server.ts:125](https://github.com/tangle-network/agent-runtime/blob/main/src/mcp/server.ts#L125) +Defined in: [mcp/server.ts:139](https://github.com/tangle-network/agent-runtime/blob/main/src/mcp/server.ts#L139) **`Experimental`** @@ -3033,7 +3046,7 @@ The underlying queue — exposed so tests can introspect it. > `readonly` **feedbackStore**: [`FeedbackStore`](#feedbackstore) -Defined in: [mcp/server.ts:127](https://github.com/tangle-network/agent-runtime/blob/main/src/mcp/server.ts#L127) +Defined in: [mcp/server.ts:141](https://github.com/tangle-network/agent-runtime/blob/main/src/mcp/server.ts#L141) **`Experimental`** @@ -3045,7 +3058,7 @@ The feedback store — exposed for the same reason. > **handle**(`message`): `Promise`\<[`JsonRpcResponse`](#jsonrpcresponse) \| `null`\> -Defined in: [mcp/server.ts:129](https://github.com/tangle-network/agent-runtime/blob/main/src/mcp/server.ts#L129) +Defined in: [mcp/server.ts:143](https://github.com/tangle-network/agent-runtime/blob/main/src/mcp/server.ts#L143) **`Experimental`** @@ -3065,7 +3078,7 @@ Handle a single parsed JSON-RPC message. Returns the response object (or `null` > **serve**(`transport?`): `Promise`\<`void`\> -Defined in: [mcp/server.ts:131](https://github.com/tangle-network/agent-runtime/blob/main/src/mcp/server.ts#L131) +Defined in: [mcp/server.ts:145](https://github.com/tangle-network/agent-runtime/blob/main/src/mcp/server.ts#L145) **`Experimental`** @@ -3085,7 +3098,7 @@ Drive the server on a stdio-shaped transport until `stop()` is called. > **stop**(): `void` -Defined in: [mcp/server.ts:133](https://github.com/tangle-network/agent-runtime/blob/main/src/mcp/server.ts#L133) +Defined in: [mcp/server.ts:147](https://github.com/tangle-network/agent-runtime/blob/main/src/mcp/server.ts#L147) **`Experimental`** @@ -3099,7 +3112,7 @@ Stop a `serve` call. Subsequent requests are rejected. ### McpTransport -Defined in: [mcp/server.ts:137](https://github.com/tangle-network/agent-runtime/blob/main/src/mcp/server.ts#L137) +Defined in: [mcp/server.ts:151](https://github.com/tangle-network/agent-runtime/blob/main/src/mcp/server.ts#L151) **`Experimental`** @@ -3109,7 +3122,7 @@ Defined in: [mcp/server.ts:137](https://github.com/tangle-network/agent-runtime/ > **input**: `ReadableStream` -Defined in: [mcp/server.ts:138](https://github.com/tangle-network/agent-runtime/blob/main/src/mcp/server.ts#L138) +Defined in: [mcp/server.ts:152](https://github.com/tangle-network/agent-runtime/blob/main/src/mcp/server.ts#L152) **`Experimental`** @@ -3117,7 +3130,7 @@ Defined in: [mcp/server.ts:138](https://github.com/tangle-network/agent-runtime/ > **output**: `WritableStream` -Defined in: [mcp/server.ts:139](https://github.com/tangle-network/agent-runtime/blob/main/src/mcp/server.ts#L139) +Defined in: [mcp/server.ts:153](https://github.com/tangle-network/agent-runtime/blob/main/src/mcp/server.ts#L153) **`Experimental`** @@ -3125,7 +3138,7 @@ Defined in: [mcp/server.ts:139](https://github.com/tangle-network/agent-runtime/ ### JsonRpcMessage -Defined in: [mcp/server.ts:143](https://github.com/tangle-network/agent-runtime/blob/main/src/mcp/server.ts#L143) +Defined in: [mcp/server.ts:157](https://github.com/tangle-network/agent-runtime/blob/main/src/mcp/server.ts#L157) **`Experimental`** @@ -3135,7 +3148,7 @@ Defined in: [mcp/server.ts:143](https://github.com/tangle-network/agent-runtime/ > **jsonrpc**: `"2.0"` -Defined in: [mcp/server.ts:144](https://github.com/tangle-network/agent-runtime/blob/main/src/mcp/server.ts#L144) +Defined in: [mcp/server.ts:158](https://github.com/tangle-network/agent-runtime/blob/main/src/mcp/server.ts#L158) **`Experimental`** @@ -3143,7 +3156,7 @@ Defined in: [mcp/server.ts:144](https://github.com/tangle-network/agent-runtime/ > `optional` **id?**: `string` \| `number` \| `null` -Defined in: [mcp/server.ts:145](https://github.com/tangle-network/agent-runtime/blob/main/src/mcp/server.ts#L145) +Defined in: [mcp/server.ts:159](https://github.com/tangle-network/agent-runtime/blob/main/src/mcp/server.ts#L159) **`Experimental`** @@ -3151,7 +3164,7 @@ Defined in: [mcp/server.ts:145](https://github.com/tangle-network/agent-runtime/ > **method**: `string` -Defined in: [mcp/server.ts:146](https://github.com/tangle-network/agent-runtime/blob/main/src/mcp/server.ts#L146) +Defined in: [mcp/server.ts:160](https://github.com/tangle-network/agent-runtime/blob/main/src/mcp/server.ts#L160) **`Experimental`** @@ -3159,7 +3172,7 @@ Defined in: [mcp/server.ts:146](https://github.com/tangle-network/agent-runtime/ > `optional` **params?**: `unknown` -Defined in: [mcp/server.ts:147](https://github.com/tangle-network/agent-runtime/blob/main/src/mcp/server.ts#L147) +Defined in: [mcp/server.ts:161](https://github.com/tangle-network/agent-runtime/blob/main/src/mcp/server.ts#L161) **`Experimental`** @@ -3167,7 +3180,7 @@ Defined in: [mcp/server.ts:147](https://github.com/tangle-network/agent-runtime/ ### JsonRpcResponse -Defined in: [mcp/server.ts:151](https://github.com/tangle-network/agent-runtime/blob/main/src/mcp/server.ts#L151) +Defined in: [mcp/server.ts:165](https://github.com/tangle-network/agent-runtime/blob/main/src/mcp/server.ts#L165) **`Experimental`** @@ -3177,7 +3190,7 @@ Defined in: [mcp/server.ts:151](https://github.com/tangle-network/agent-runtime/ > **jsonrpc**: `"2.0"` -Defined in: [mcp/server.ts:152](https://github.com/tangle-network/agent-runtime/blob/main/src/mcp/server.ts#L152) +Defined in: [mcp/server.ts:166](https://github.com/tangle-network/agent-runtime/blob/main/src/mcp/server.ts#L166) **`Experimental`** @@ -3185,7 +3198,7 @@ Defined in: [mcp/server.ts:152](https://github.com/tangle-network/agent-runtime/ > **id**: `string` \| `number` \| `null` -Defined in: [mcp/server.ts:153](https://github.com/tangle-network/agent-runtime/blob/main/src/mcp/server.ts#L153) +Defined in: [mcp/server.ts:167](https://github.com/tangle-network/agent-runtime/blob/main/src/mcp/server.ts#L167) **`Experimental`** @@ -3193,7 +3206,7 @@ Defined in: [mcp/server.ts:153](https://github.com/tangle-network/agent-runtime/ > `optional` **result?**: `unknown` -Defined in: [mcp/server.ts:154](https://github.com/tangle-network/agent-runtime/blob/main/src/mcp/server.ts#L154) +Defined in: [mcp/server.ts:168](https://github.com/tangle-network/agent-runtime/blob/main/src/mcp/server.ts#L168) **`Experimental`** @@ -3201,7 +3214,7 @@ Defined in: [mcp/server.ts:154](https://github.com/tangle-network/agent-runtime/ > `optional` **error?**: `object` -Defined in: [mcp/server.ts:155](https://github.com/tangle-network/agent-runtime/blob/main/src/mcp/server.ts#L155) +Defined in: [mcp/server.ts:169](https://github.com/tangle-network/agent-runtime/blob/main/src/mcp/server.ts#L169) **`Experimental`** @@ -4162,7 +4175,7 @@ Defined in: [mcp/tools/coordination.ts:97](https://github.com/tangle-network/age ##### perWorker -> `readonly` **perWorker**: [`Budget`](runtime.md#budget-9) +> `readonly` **perWorker**: [`Budget`](runtime.md#budget-10) Defined in: [mcp/tools/coordination.ts:98](https://github.com/tangle-network/agent-runtime/blob/main/src/mcp/tools/coordination.ts#L98) @@ -4315,6 +4328,94 @@ Raise a `finding` on the bus from outside the settle hook — the seam an ONLINE *** +### DelegateArgs + +Defined in: [mcp/tools/delegate.ts:71](https://github.com/tangle-network/agent-runtime/blob/main/src/mcp/tools/delegate.ts#L71) + +Parsed `delegate` tool arguments. + +#### Properties + +##### intent + +> **intent**: `string` + +Defined in: [mcp/tools/delegate.ts:72](https://github.com/tangle-network/agent-runtime/blob/main/src/mcp/tools/delegate.ts#L72) + +##### model? + +> `optional` **model?**: `string` + +Defined in: [mcp/tools/delegate.ts:73](https://github.com/tangle-network/agent-runtime/blob/main/src/mcp/tools/delegate.ts#L73) + +##### runId? + +> `optional` **runId?**: `string` + +Defined in: [mcp/tools/delegate.ts:74](https://github.com/tangle-network/agent-runtime/blob/main/src/mcp/tools/delegate.ts#L74) + +*** + +### DelegateHandlerOptions + +Defined in: [mcp/tools/delegate.ts:106](https://github.com/tangle-network/agent-runtime/blob/main/src/mcp/tools/delegate.ts#L106) + +**`Experimental`** + +#### Properties + +##### router + +> **router**: [`RouterConfig`](runtime.md#routerconfig) + +Defined in: [mcp/tools/delegate.ts:108](https://github.com/tangle-network/agent-runtime/blob/main/src/mcp/tools/delegate.ts#L108) + +**`Experimental`** + +The supervisor brain's router substrate (REQUIRED — the default supervisor is router-brained). + +##### backend + +> **backend**: [`ExecutorConfig`](runtime.md#executorconfig) + +Defined in: [mcp/tools/delegate.ts:110](https://github.com/tangle-network/agent-runtime/blob/main/src/mcp/tools/delegate.ts#L110) + +**`Experimental`** + +WHERE the authored workers run. Required for `supervise()` to spawn anything. + +##### deliverable? + +> `optional` **deliverable?**: [`DeliverableSpec`](runtime.md#deliverablespec)\<`unknown`\> + +Defined in: [mcp/tools/delegate.ts:112](https://github.com/tangle-network/agent-runtime/blob/main/src/mcp/tools/delegate.ts#L112) + +**`Experimental`** + +The completion oracle the authored workers settle against (settled ⟺ delivered). + +##### model? + +> `optional` **model?**: `string` + +Defined in: [mcp/tools/delegate.ts:114](https://github.com/tangle-network/agent-runtime/blob/main/src/mcp/tools/delegate.ts#L114) + +**`Experimental`** + +Default supervisor brain model when a call omits `model`. + +##### allowedModels? + +> `optional` **allowedModels?**: readonly `string`[] + +Defined in: [mcp/tools/delegate.ts:116](https://github.com/tangle-network/agent-runtime/blob/main/src/mcp/tools/delegate.ts#L116) + +**`Experimental`** + +Restrict the run to this subset of models. + +*** + ### TraceContext Defined in: [mcp/trace-propagation.ts:24](https://github.com/tangle-network/agent-runtime/blob/main/src/mcp/trace-propagation.ts#L24) @@ -5899,6 +6000,17 @@ Defined in: [mcp/tools/coordination.ts:92](https://github.com/tangle-network/age *** +### DelegateResult + +> **DelegateResult** = \{ `status`: `"winner"`; `out`: `unknown`; `outRef`: `string`; `spentTotal`: [`Spend`](runtime.md#spend); \} \| \{ `status`: `"no-winner"`; `reason`: `string`; `spentTotal`: [`Spend`](runtime.md#spend); \} + +Defined in: [mcp/tools/delegate.ts:101](https://github.com/tangle-network/agent-runtime/blob/main/src/mcp/tools/delegate.ts#L101) + +The synchronous result the `delegate` tool returns to the calling agent: the delivered output (or + the no-winner reason) PLUS the conserved spend of the whole delegation. + +*** + ### DelegationProfile > **DelegationProfile** = `"coder"` \| `"researcher"` \| `"ui-auditor"` @@ -6853,6 +6965,90 @@ Defined in: [mcp/tools/delegate-ui-audit.ts:86](https://github.com/tangle-networ *** +### DELEGATE\_TOOL\_NAME + +> `const` **DELEGATE\_TOOL\_NAME**: `"delegate"` = `'delegate'` + +Defined in: [mcp/tools/delegate.ts:29](https://github.com/tangle-network/agent-runtime/blob/main/src/mcp/tools/delegate.ts#L29) + +**`Experimental`** + +*** + +### DELEGATE\_DESCRIPTION + +> `const` **DELEGATE\_DESCRIPTION**: `string` + +Defined in: [mcp/tools/delegate.ts:32](https://github.com/tangle-network/agent-runtime/blob/main/src/mcp/tools/delegate.ts#L32) + +**`Experimental`** + +*** + +### DELEGATE\_INPUT\_SCHEMA + +> `const` **DELEGATE\_INPUT\_SCHEMA**: `object` + +Defined in: [mcp/tools/delegate.ts:50](https://github.com/tangle-network/agent-runtime/blob/main/src/mcp/tools/delegate.ts#L50) + +**`Experimental`** + +#### Type Declaration + +##### type + +> `readonly` **type**: `"object"` = `'object'` + +##### properties + +> `readonly` **properties**: `object` + +###### properties.intent + +> `readonly` **intent**: `object` + +###### properties.intent.type + +> `readonly` **type**: `"string"` = `'string'` + +###### properties.intent.description + +> `readonly` **description**: `"What you want accomplished, as an outcome. The supervisor authors the worker."` = `'What you want accomplished, as an outcome. The supervisor authors the worker.'` + +###### properties.model + +> `readonly` **model**: `object` + +###### properties.model.type + +> `readonly` **type**: `"string"` = `'string'` + +###### properties.model.description + +> `readonly` **description**: `"Optional per-call override for the supervisor brain model."` = `'Optional per-call override for the supervisor brain model.'` + +###### properties.runId + +> `readonly` **runId**: `object` + +###### properties.runId.type + +> `readonly` **type**: `"string"` = `'string'` + +###### properties.runId.description + +> `readonly` **description**: `"Optional trace-correlation id for this delegation."` = `'Optional trace-correlation id for this delegation.'` + +##### required + +> `readonly` **required**: readonly \[`"intent"`\] + +##### additionalProperties + +> `readonly` **additionalProperties**: `false` = `false` + +*** + ### DELEGATION\_HISTORY\_TOOL\_NAME > `const` **DELEGATION\_HISTORY\_TOOL\_NAME**: `"delegation_history"` = `'delegation_history'` @@ -7608,7 +7804,7 @@ Does NOT throw when: > **createMcpServer**(`options?`): [`McpServer`](#mcpserver) -Defined in: [mcp/server.ts:163](https://github.com/tangle-network/agent-runtime/blob/main/src/mcp/server.ts#L163) +Defined in: [mcp/server.ts:177](https://github.com/tangle-network/agent-runtime/blob/main/src/mcp/server.ts#L177) **`Experimental`** @@ -7628,7 +7824,7 @@ Defined in: [mcp/server.ts:163](https://github.com/tangle-network/agent-runtime/ > **createInProcessTransport**(): `object` -Defined in: [mcp/server.ts:367](https://github.com/tangle-network/agent-runtime/blob/main/src/mcp/server.ts#L367) +Defined in: [mcp/server.ts:389](https://github.com/tangle-network/agent-runtime/blob/main/src/mcp/server.ts#L389) **`Experimental`** @@ -7988,6 +8184,48 @@ Defined in: [mcp/tools/delegate-ui-audit.ts:300](https://github.com/tangle-netwo *** +### validateDelegateArgs() + +> **validateDelegateArgs**(`raw`): [`DelegateArgs`](#delegateargs) + +Defined in: [mcp/tools/delegate.ts:78](https://github.com/tangle-network/agent-runtime/blob/main/src/mcp/tools/delegate.ts#L78) + +**`Experimental`** + +#### Parameters + +##### raw + +`unknown` + +#### Returns + +[`DelegateArgs`](#delegateargs) + +*** + +### createDelegateHandler() + +> **createDelegateHandler**(`options`): (`raw`) => `Promise`\<[`DelegateResult`](#delegateresult)\> + +Defined in: [mcp/tools/delegate.ts:140](https://github.com/tangle-network/agent-runtime/blob/main/src/mcp/tools/delegate.ts#L140) + +Build the `delegate` tool handler. Closes over the injected supervisor substrate (`router` / +`backend` / `deliverable`); each call routes the agent's intent to `delegate()` and returns the +delivered output with its conserved cost. + +#### Parameters + +##### options + +[`DelegateHandlerOptions`](#delegatehandleroptions) + +#### Returns + +(`raw`) => `Promise`\<[`DelegateResult`](#delegateresult)\> + +*** + ### validateDelegationHistoryArgs() > **validateDelegationHistoryArgs**(`raw`): [`DelegationHistoryArgs`](#delegationhistoryargs) diff --git a/docs/api/runtime.md b/docs/api/runtime.md index 28348ee5..6d44a097 100644 --- a/docs/api/runtime.md +++ b/docs/api/runtime.md @@ -1333,7 +1333,7 @@ The analyst agent the combinator spawns over the trace. `harness` is the persona ##### budget -> `readonly` **budget**: [`Budget`](#budget-9) +> `readonly` **budget**: [`Budget`](#budget-10) Defined in: [runtime/personify/analyst.ts:78](https://github.com/tangle-network/agent-runtime/blob/main/src/runtime/personify/analyst.ts#L78) @@ -1639,7 +1639,7 @@ against them and fails closed, so an over-eager shape can never overspend. ##### perChild -> `readonly` **perChild**: [`Budget`](#budget-9) +> `readonly` **perChild**: [`Budget`](#budget-10) Defined in: [runtime/personify/types.ts:155](https://github.com/tangle-network/agent-runtime/blob/main/src/runtime/personify/types.ts#L155) @@ -1873,7 +1873,7 @@ Defined in: [runtime/personify/types.ts:226](https://github.com/tangle-network/a ##### budget -> `readonly` **budget**: [`Budget`](#budget-9) +> `readonly` **budget**: [`Budget`](#budget-10) Defined in: [runtime/personify/types.ts:227](https://github.com/tangle-network/agent-runtime/blob/main/src/runtime/personify/types.ts#L227) @@ -6519,7 +6519,7 @@ budget: refine→max shots; sample→rollout width. ##### rootBudget? -> `optional` **rootBudget?**: [`Budget`](#budget-9) +> `optional` **rootBudget?**: [`Budget`](#budget-10) Defined in: [runtime/strategy.ts:977](https://github.com/tangle-network/agent-runtime/blob/main/src/runtime/strategy.ts#L977) @@ -6735,7 +6735,7 @@ caller inspects `ok` before `ticket`. ###### b -[`Budget`](#budget-9) +[`Budget`](#budget-10) ###### Returns @@ -6923,7 +6923,7 @@ Resolve a spawned `profile` to a worker LEAF or a driver child (the recursion se ##### perWorker -> `readonly` **perWorker**: [`Budget`](#budget-9) +> `readonly` **perWorker**: [`Budget`](#budget-10) Defined in: [runtime/supervise/coordination-driver.ts:50](https://github.com/tangle-network/agent-runtime/blob/main/src/runtime/supervise/coordination-driver.ts#L50) @@ -7106,6 +7106,101 @@ Defined in: [runtime/supervise/coordination-mcp.ts:46](https://github.com/tangle *** +### DelegateOptions + +Defined in: [runtime/supervise/delegate.ts:37](https://github.com/tangle-network/agent-runtime/blob/main/src/runtime/supervise/delegate.ts#L37) + +Inputs to [delegate](#delegate). The intent is the first positional arg; everything here is optional + with sensible defaults, so the common call is `delegate(intent, { backend, router })`. + +#### Type Parameters + +##### Out + +`Out` = `unknown` + +#### Properties + +##### deliverable? + +> `readonly` `optional` **deliverable?**: [`DeliverableSpec`](#deliverablespec)\<`Out`\> + +Defined in: [runtime/supervise/delegate.ts:41](https://github.com/tangle-network/agent-runtime/blob/main/src/runtime/supervise/delegate.ts#L41) + +The completion oracle (settled ⟺ delivered) the authored workers settle against. Strongly + recommended — without it the supervisor trusts a worker's self-report. For a code intent, + `patchDelivered()` is the canonical example; for a free-form answer, a content check. + +##### backend? + +> `readonly` `optional` **backend?**: [`ExecutorConfig`](#executorconfig) + +Defined in: [runtime/supervise/delegate.ts:45](https://github.com/tangle-network/agent-runtime/blob/main/src/runtime/supervise/delegate.ts#L45) + +WHERE the authored workers run — the worker-execution backend (`router-tools` / `sandbox` / + `cli-worktree` / …). The supervisor authors the worker PROFILE; this is the substrate it runs + on. Provide this OR `makeWorkerAgent`-style wiring through `supervise()` is unavailable. + +##### budget? + +> `readonly` `optional` **budget?**: [`Budget`](#budget-10) + +Defined in: [runtime/supervise/delegate.ts:47](https://github.com/tangle-network/agent-runtime/blob/main/src/runtime/supervise/delegate.ts#L47) + +The conserved compute pool for the whole delegation. Defaults to [defaultDelegateBudget](#defaultdelegatebudget). + +##### model? + +> `readonly` `optional` **model?**: `string` + +Defined in: [runtime/supervise/delegate.ts:50](https://github.com/tangle-network/agent-runtime/blob/main/src/runtime/supervise/delegate.ts#L50) + +The model the supervisor BRAIN runs on (the router model). The brain must tool-call + (`spawn_agent` / `await_event`), so a delegator model, not a hidden-reasoning model. + +##### router? + +> `readonly` `optional` **router?**: [`RouterConfig`](#routerconfig) + +Defined in: [runtime/supervise/delegate.ts:54](https://github.com/tangle-network/agent-runtime/blob/main/src/runtime/supervise/delegate.ts#L54) + +The supervisor brain's router substrate. REQUIRED for the default router-brained supervisor + (the brain is resolved from this), unless a test injects `brain` directly. `model` overrides + `router.model`. (Design delta vs the bare `supervise()` profile: the brain needs a router.) + +##### brain? + +> `readonly` `optional` **brain?**: [`ToolLoopChat`](#toolloopchat) + +Defined in: [runtime/supervise/delegate.ts:56](https://github.com/tangle-network/agent-runtime/blob/main/src/runtime/supervise/delegate.ts#L56) + +Inject the supervisor brain directly (tests / advanced) instead of resolving it from `router`. + +##### supervisor? + +> `readonly` `optional` **supervisor?**: `Partial`\<`Pick`\<[`SupervisorProfile`](#supervisorprofile), `"name"` \| `"systemPrompt"`\>\> + +Defined in: [runtime/supervise/delegate.ts:59](https://github.com/tangle-network/agent-runtime/blob/main/src/runtime/supervise/delegate.ts#L59) + +Override the default authoring-supervisor profile (name / extra system-prompt stance). The + default already carries the authoring skill; override only to add a goal or rename. + +##### allowedModels? + +> `readonly` `optional` **allowedModels?**: readonly `string`[] + +Defined in: [runtime/supervise/delegate.ts:61](https://github.com/tangle-network/agent-runtime/blob/main/src/runtime/supervise/delegate.ts#L61) + +Restrict the run to this subset of models (forwarded to `supervise()`). + +##### runId? + +> `readonly` `optional` **runId?**: `string` + +Defined in: [runtime/supervise/delegate.ts:62](https://github.com/tangle-network/agent-runtime/blob/main/src/runtime/supervise/delegate.ts#L62) + +*** + ### WatchTraceOptions Defined in: [runtime/supervise/detector-monitor.ts:22](https://github.com/tangle-network/agent-runtime/blob/main/src/runtime/supervise/detector-monitor.ts#L22) @@ -7628,7 +7723,7 @@ Defined in: [runtime/supervise/supervise.ts:46](https://github.com/tangle-networ ##### budget -> `readonly` **budget**: [`Budget`](#budget-9) +> `readonly` **budget**: [`Budget`](#budget-10) Defined in: [runtime/supervise/supervise.ts:48](https://github.com/tangle-network/agent-runtime/blob/main/src/runtime/supervise/supervise.ts#L48) @@ -7718,7 +7813,7 @@ Runs an `extraTools` call; null/undefined falls through to the coordination disp ##### perWorker? -> `readonly` `optional` **perWorker?**: [`Budget`](#budget-9) +> `readonly` `optional` **perWorker?**: [`Budget`](#budget-10) Defined in: [runtime/supervise/supervise.ts:77](https://github.com/tangle-network/agent-runtime/blob/main/src/runtime/supervise/supervise.ts#L77) @@ -7835,7 +7930,7 @@ Resolve a spawned worker `profile` to a leaf agent — the recursion seam (same ##### perWorker -> `readonly` **perWorker**: [`Budget`](#budget-9) +> `readonly` **perWorker**: [`Budget`](#budget-10) Defined in: [runtime/supervise/supervisor-agent.ts:52](https://github.com/tangle-network/agent-runtime/blob/main/src/runtime/supervise/supervisor-agent.ts#L52) @@ -8693,7 +8788,7 @@ Defined in: [runtime/supervise/types.ts:432](https://github.com/tangle-network/a ##### budget -> `readonly` **budget**: [`Budget`](#budget-9) +> `readonly` **budget**: [`Budget`](#budget-10) Defined in: [runtime/supervise/types.ts:434](https://github.com/tangle-network/agent-runtime/blob/main/src/runtime/supervise/types.ts#L434) @@ -12124,6 +12219,18 @@ Defined in: [runtime/supervise/authoring.ts:138](https://github.com/tangle-netwo *** +### defaultDelegateBudget + +> `const` **defaultDelegateBudget**: [`Budget`](#budget-10) + +Defined in: [runtime/supervise/delegate.ts:33](https://github.com/tangle-network/agent-runtime/blob/main/src/runtime/supervise/delegate.ts#L33) + +The conserved pool a `delegate()` call applies when the caller does not pass its own `budget`. + A modest token ceiling + a small iteration ceiling — generous enough for a few-worker decompose, + bounded enough that an unsupervised intent cannot run away. Callers override via `opts.budget`. + +*** + ### cliWorktreeExecutor > `const` **cliWorktreeExecutor**: `ExecutorFactory`\<`unknown`\> @@ -14132,7 +14239,7 @@ readout's `deadlineMs` is a stable wall-clock instant, not a shrinking remainder ##### root -[`Budget`](#budget-9) +[`Budget`](#budget-10) ##### now? @@ -14254,7 +14361,7 @@ Stand up the coordination MCP over a live scope. The HOST address is `127.0.0.1` ###### perWorker -[`Budget`](#budget-9) +[`Budget`](#budget-10) ###### port? @@ -14292,6 +14399,40 @@ Pass-through subscriber for every bus event (settled / question / finding). *** +### delegate() + +> **delegate**\<`Out`\>(`intent`, `opts?`): `Promise`\<[`SupervisedResult`](#supervisedresult)\<`Out`\>\> + +Defined in: [runtime/supervise/delegate.ts:87](https://github.com/tangle-network/agent-runtime/blob/main/src/runtime/supervise/delegate.ts#L87) + +Delegate an INTENT to a default authoring supervisor and return its `SupervisedResult` unchanged. + +The supervisor authors + spawns whatever worker the intent needs over the conserved-budget pool; +`result.spentTotal` reports what the whole delegation actually cost. A `winner` result carries the +authored worker's delivered output; a `no-winner` result names why (never a fabricated success). + +#### Type Parameters + +##### Out + +`Out` = `unknown` + +#### Parameters + +##### intent + +`string` + +##### opts? + +[`DelegateOptions`](#delegateoptions)\<`Out`\> = `{}` + +#### Returns + +`Promise`\<[`SupervisedResult`](#supervisedresult)\<`Out`\>\> + +*** + ### defaultToolDetectors() > **defaultToolDetectors**(): `StreamingDetector`[] @@ -14626,7 +14767,7 @@ Defined in: [runtime/supervise/supervisor-agent.ts:74](https://github.com/tangle ### createSupervisor() -> **createSupervisor**\<`Task`, `Out`\>(): [`Supervisor`](#supervisor)\<`Task`, `Out`\> +> **createSupervisor**\<`Task`, `Out`\>(): [`Supervisor`](#supervisor-1)\<`Task`, `Out`\> Defined in: [runtime/supervise/supervisor.ts:64](https://github.com/tangle-network/agent-runtime/blob/main/src/runtime/supervise/supervisor.ts#L64) @@ -14642,7 +14783,7 @@ Defined in: [runtime/supervise/supervisor.ts:64](https://github.com/tangle-netwo #### Returns -[`Supervisor`](#supervisor)\<`Task`, `Out`\> +[`Supervisor`](#supervisor-1)\<`Task`, `Out`\> *** diff --git a/src/mcp/index.ts b/src/mcp/index.ts index cae16c5f..8e36a43c 100644 --- a/src/mcp/index.ts +++ b/src/mcp/index.ts @@ -1,10 +1,12 @@ /** * @experimental * - * `@tangle-network/agent-runtime/mcp` — Stdio MCP server exposing the 5 - * delegation tools (`delegate_code`, `delegate_research`, - * `delegate_feedback`, `delegation_status`, `delegation_history`) to - * sandbox coding-harness agents. + * `@tangle-network/agent-runtime/mcp` — Stdio MCP server exposing the + * delegation tools to sandbox coding-harness agents: the generic `delegate` + * (one intent → a supervisor that authors + drives its own worker, returns the + * delivered output with its cost), plus the task-specific `delegate_code`, + * `delegate_research`, `delegate_feedback`, `delegation_status`, and + * `delegation_history`. * * Mount the server inside a product agent's sandbox via * `agent-runtime-mcp` (the bin) or wire it into a custom Node entry @@ -143,6 +145,16 @@ export { type QuestionRecord, type SettledWorker, } from './tools/coordination' +export { + createDelegateHandler, + DELEGATE_DESCRIPTION, + DELEGATE_INPUT_SCHEMA, + DELEGATE_TOOL_NAME, + type DelegateArgs, + type DelegateHandlerOptions, + type DelegateResult, + validateDelegateArgs, +} from './tools/delegate' export { createDelegateCodeHandler, DELEGATE_CODE_DESCRIPTION, diff --git a/src/mcp/server.ts b/src/mcp/server.ts index e6b87994..dbd60f4c 100644 --- a/src/mcp/server.ts +++ b/src/mcp/server.ts @@ -22,6 +22,13 @@ import { ValidationError } from '../errors' import type { CoderDelegate, ResearcherDelegate, UiAuditorDelegate } from './delegates' import { type FeedbackStore, InMemoryFeedbackStore } from './feedback-store' import { DelegationTaskQueue } from './task-queue' +import { + createDelegateHandler, + DELEGATE_DESCRIPTION, + DELEGATE_INPUT_SCHEMA, + DELEGATE_TOOL_NAME, + type DelegateHandlerOptions, +} from './tools/delegate' import { createDelegateCodeHandler, DELEGATE_CODE_DESCRIPTION, @@ -62,6 +69,13 @@ import type { TraceContext } from './trace-propagation' /** @experimental */ export interface McpServerOptions { + /** + * Required to enable `delegate` — the ONE generic delegation verb (the replacement for + * delegate_code / delegate_research). Inject the supervisor substrate: its brain `router`, the + * worker `backend`, and the completion `deliverable`. The supervisor AUTHORS its own worker from + * the agent's intent, so there is no worker profile to wire here. + */ + delegateSupervisor?: DelegateHandlerOptions /** Required to enable delegate_code. */ coderDelegate?: CoderDelegate /** @@ -172,6 +186,14 @@ export function createMcpServer(options: McpServerOptions = {}): McpServer { const tools = new Map() + if (options.delegateSupervisor) { + tools.set(DELEGATE_TOOL_NAME, { + name: DELEGATE_TOOL_NAME, + description: DELEGATE_DESCRIPTION, + inputSchema: DELEGATE_INPUT_SCHEMA as unknown as Record, + handler: createDelegateHandler(options.delegateSupervisor), + }) + } if (options.coderDelegate) { tools.set(DELEGATE_CODE_TOOL_NAME, { name: DELEGATE_CODE_TOOL_NAME, diff --git a/src/mcp/tools/delegate.ts b/src/mcp/tools/delegate.ts new file mode 100644 index 00000000..837c9ae2 --- /dev/null +++ b/src/mcp/tools/delegate.ts @@ -0,0 +1,156 @@ +/** + * @experimental + * + * `delegate` MCP tool — the ONE generic delegation verb, the agent-facing front door to + * `delegate()` / `supervise()`. The agent hands it an INTENT (what it wants done); a default + * authoring supervisor decomposes the intent and AUTHORS the worker profile it needs — there is no + * hardcoded coder/researcher profile. It is the generic replacement for `delegate_code` / + * `delegate_research`. + * + * Unlike those async, queue-backed tools (kick off → return a taskId → poll `delegation_status`), + * `delegate` is SYNCHRONOUS: it awaits the full supervised run and returns the delivered output + * TOGETHER WITH `spentTotal` — the conserved cost of the whole delegation (`iterations` / `tokens` / + * `usd` / `ms`). Returning the real spend is the whole reason `delegate` beats `delegate_code`, + * which has no cost channel. + * + * The supervisor's substrate (its brain `router`, the worker `backend`, the completion `deliverable`) + * is INJECTED at server construction — never an agent-supplied arg — exactly as `delegate_code` + * injects its `CoderDelegate`. The agent supplies only the intent (+ an optional per-call `model` / + * `runId`). + */ + +import type { RouterConfig } from '../../runtime/router-client' +import type { DeliverableSpec } from '../../runtime/supervise/completion-gate' +import { type DelegateOptions, delegate } from '../../runtime/supervise/delegate' +import type { ExecutorConfig } from '../../runtime/supervise/runtime' +import type { Spend, SupervisedResult } from '../../runtime/supervise/types' + +/** @experimental */ +export const DELEGATE_TOOL_NAME = 'delegate' + +/** @experimental */ +export const DELEGATE_DESCRIPTION = [ + 'Delegate an INTENT to a supervisor that AUTHORS and drives whatever worker the intent needs.', + '', + 'Use when: you want a task done but do not want to specify HOW. State the outcome — "fix the', + 'failing auth test", "research competitor pricing with citations", "refactor the parser for', + 'clarity" — and the supervisor decomposes it, writes a tailored worker profile per sub-task, runs', + 'the workers over a conserved compute budget, and settles only when a deployable check passes.', + '', + 'There is no fixed worker type: this ONE verb replaces separate code / research delegation. The', + 'supervisor picks the worker shape from your intent.', + '', + 'Returns synchronously with the delivered result AND the real cost of the whole delegation', + '(spentTotal: iterations, input/output tokens, usd, ms) — so you always know what it spent. A run', + 'that produced no delivered worker returns status "no-winner" with the reason; it never fabricates', + 'a success.', +].join('\n') + +/** @experimental */ +export const DELEGATE_INPUT_SCHEMA = { + type: 'object', + properties: { + intent: { + type: 'string', + description: 'What you want accomplished, as an outcome. The supervisor authors the worker.', + }, + model: { + type: 'string', + description: 'Optional per-call override for the supervisor brain model.', + }, + runId: { + type: 'string', + description: 'Optional trace-correlation id for this delegation.', + }, + }, + required: ['intent'], + additionalProperties: false, +} as const + +/** Parsed `delegate` tool arguments. */ +export interface DelegateArgs { + intent: string + model?: string + runId?: string +} + +/** @experimental */ +export function validateDelegateArgs(raw: unknown): DelegateArgs { + if (raw === null || typeof raw !== 'object') { + throw new TypeError('delegate: arguments must be an object') + } + const value = raw as Record + const intent = value.intent + if (typeof intent !== 'string' || intent.trim().length === 0) { + throw new TypeError('delegate: `intent` must be a non-empty string') + } + const args: DelegateArgs = { intent: intent.trim() } + if (value.model !== undefined) { + if (typeof value.model !== 'string') throw new TypeError('delegate: `model` must be a string') + args.model = value.model + } + if (value.runId !== undefined) { + if (typeof value.runId !== 'string') throw new TypeError('delegate: `runId` must be a string') + args.runId = value.runId + } + return args +} + +/** The synchronous result the `delegate` tool returns to the calling agent: the delivered output (or + * the no-winner reason) PLUS the conserved spend of the whole delegation. */ +export type DelegateResult = + | { status: 'winner'; out: unknown; outRef: string; spentTotal: Spend } + | { status: 'no-winner'; reason: string; spentTotal: Spend } + +/** @experimental */ +export interface DelegateHandlerOptions { + /** The supervisor brain's router substrate (REQUIRED — the default supervisor is router-brained). */ + router: RouterConfig + /** WHERE the authored workers run. Required for `supervise()` to spawn anything. */ + backend: ExecutorConfig + /** The completion oracle the authored workers settle against (settled ⟺ delivered). */ + deliverable?: DeliverableSpec + /** Default supervisor brain model when a call omits `model`. */ + model?: string + /** Restrict the run to this subset of models. */ + allowedModels?: readonly string[] +} + +const zeroSpend: Spend = { iterations: 0, tokens: { input: 0, output: 0 }, usd: 0, ms: 0 } + +/** Project a `SupervisedResult` onto the tool's flat `DelegateResult`. A no-winner carries no spend + * on the result union, so it reports zero spend with the failure reason — never a faked output. */ +function toDelegateResult(result: SupervisedResult): DelegateResult { + if (result.kind === 'no-winner') { + return { status: 'no-winner', reason: result.reason, spentTotal: zeroSpend } + } + return { + status: 'winner', + out: result.out, + outRef: result.outRef, + spentTotal: result.spentTotal, + } +} + +/** + * Build the `delegate` tool handler. Closes over the injected supervisor substrate (`router` / + * `backend` / `deliverable`); each call routes the agent's intent to `delegate()` and returns the + * delivered output with its conserved cost. + */ +export function createDelegateHandler( + options: DelegateHandlerOptions, +): (raw: unknown) => Promise { + return async (raw) => { + const args = validateDelegateArgs(raw) + const opts: DelegateOptions = { + backend: options.backend, + router: options.router, + model: args.model ?? options.model, + ...(options.deliverable ? { deliverable: options.deliverable } : {}), + ...(options.allowedModels ? { allowedModels: options.allowedModels } : {}), + ...(args.runId ? { runId: args.runId } : {}), + } + const result = await delegate(args.intent, opts) + return toDelegateResult(result) + } +} diff --git a/src/runtime/index.ts b/src/runtime/index.ts index c64aa0e7..1a62feb9 100644 --- a/src/runtime/index.ts +++ b/src/runtime/index.ts @@ -315,6 +315,15 @@ export { // Supervisor-as-MCP: serve the coordination verbs as a real HTTP MCP over a live Scope, so any // harness (claude-code / codex / opencode) BECOMES the supervisor by mounting one MCP server. export { type CoordinationMcpHandle, serveCoordinationMcp } from './supervise/coordination-mcp' +// The one generic delegation verb: hand it an INTENT, it routes to `supervise()` with a default +// authoring supervisor (no hardcoded worker profile) and returns the `SupervisedResult` unchanged — +// so `spentTotal` (what the delegation cost) rides straight back. The generic replacement for +// delegate_code / delegate_research. +export { + type DelegateOptions, + defaultDelegateBudget, + delegate, +} from './supervise/delegate' // The ONLINE analyst: watch a TraceSource and raise a `finding` the moment a worker loops/error-storms. export { defaultToolDetectors, diff --git a/src/runtime/supervise/delegate.ts b/src/runtime/supervise/delegate.ts new file mode 100644 index 00000000..a712c81c --- /dev/null +++ b/src/runtime/supervise/delegate.ts @@ -0,0 +1,111 @@ +/** + * @experimental + * + * `delegate` — the one generic delegation verb. You hand it an INTENT (what you want done) and it + * hands that intent to a default AUTHORING supervisor: a router-brained supervisor whose standing + * instruction is `supervisorInstructions()` (the authoring-agent-profiles skill). The supervisor + * DECOMPOSES the intent and AUTHORS the worker profile it needs per sub-task — there is NO hardcoded + * coder/researcher profile here. That is the whole point: `delegate('fix the failing test', …)` and + * `delegate('research X and cite sources', …)` route through the SAME front door; the supervisor + * writes a code-shaped or research-shaped worker on its own. + * + * It is a thin wrapper over `supervise()` — the one front door — so the conserved-budget pool, the + * completion oracle (`deliverable`), the coordination toolbox, and equal-compute accounting all come + * for free; nothing is hand-rolled. The result is `supervise()`'s `SupervisedResult` returned + * UNCHANGED, so its `spentTotal` (`{ iterations, tokens, usd, ms }`) rides straight back to the + * caller. That cost channel is exactly what `delegate_code` lacks — a `delegate()` caller always + * learns what the delegation actually spent. + */ + +import { ConfigError } from '../../errors' +import type { RouterConfig } from '../router-client' +import type { ToolLoopChat } from '../tool-loop' +import { supervisorInstructions } from './authoring' +import type { DeliverableSpec } from './completion-gate' +import type { ExecutorConfig } from './runtime' +import { supervise } from './supervise' +import type { SupervisorProfile } from './supervisor-agent' +import type { Budget, SupervisedResult } from './types' + +/** The conserved pool a `delegate()` call applies when the caller does not pass its own `budget`. + * A modest token ceiling + a small iteration ceiling — generous enough for a few-worker decompose, + * bounded enough that an unsupervised intent cannot run away. Callers override via `opts.budget`. */ +export const defaultDelegateBudget: Budget = { maxIterations: 50, maxTokens: 200_000 } + +/** Inputs to {@link delegate}. The intent is the first positional arg; everything here is optional + * with sensible defaults, so the common call is `delegate(intent, { backend, router })`. */ +export interface DelegateOptions { + /** The completion oracle (settled ⟺ delivered) the authored workers settle against. Strongly + * recommended — without it the supervisor trusts a worker's self-report. For a code intent, + * `patchDelivered()` is the canonical example; for a free-form answer, a content check. */ + readonly deliverable?: DeliverableSpec + /** WHERE the authored workers run — the worker-execution backend (`router-tools` / `sandbox` / + * `cli-worktree` / …). The supervisor authors the worker PROFILE; this is the substrate it runs + * on. Provide this OR `makeWorkerAgent`-style wiring through `supervise()` is unavailable. */ + readonly backend?: ExecutorConfig + /** The conserved compute pool for the whole delegation. Defaults to {@link defaultDelegateBudget}. */ + readonly budget?: Budget + /** The model the supervisor BRAIN runs on (the router model). The brain must tool-call + * (`spawn_agent` / `await_event`), so a delegator model, not a hidden-reasoning model. */ + readonly model?: string + /** The supervisor brain's router substrate. REQUIRED for the default router-brained supervisor + * (the brain is resolved from this), unless a test injects `brain` directly. `model` overrides + * `router.model`. (Design delta vs the bare `supervise()` profile: the brain needs a router.) */ + readonly router?: RouterConfig + /** Inject the supervisor brain directly (tests / advanced) instead of resolving it from `router`. */ + readonly brain?: ToolLoopChat + /** Override the default authoring-supervisor profile (name / extra system-prompt stance). The + * default already carries the authoring skill; override only to add a goal or rename. */ + readonly supervisor?: Partial> + /** Restrict the run to this subset of models (forwarded to `supervise()`). */ + readonly allowedModels?: readonly string[] + readonly runId?: string +} + +/** Build the DEFAULT authoring supervisor profile: a router-brained supervisor (`harness: null`) + * whose standing instruction IS the authoring-agent-profiles skill, so it decomposes the intent and + * AUTHORS a worker profile per sub-task. No worker profile is baked in here. */ +function authoringSupervisorProfile( + model: string | undefined, + override?: Partial>, +): SupervisorProfile { + return { + name: override?.name ?? 'delegate-supervisor', + harness: null, + ...(model ? { model } : {}), + systemPrompt: override?.systemPrompt ?? supervisorInstructions(), + } +} + +/** + * Delegate an INTENT to a default authoring supervisor and return its `SupervisedResult` unchanged. + * + * The supervisor authors + spawns whatever worker the intent needs over the conserved-budget pool; + * `result.spentTotal` reports what the whole delegation actually cost. A `winner` result carries the + * authored worker's delivered output; a `no-winner` result names why (never a fabricated success). + */ +export async function delegate( + intent: string, + opts: DelegateOptions = {}, +): Promise> { + if (typeof intent !== 'string' || intent.trim().length === 0) { + throw new ConfigError('delegate: `intent` must be a non-empty string') + } + if (!opts.brain && !opts.router) { + throw new ConfigError( + 'delegate: provide opts.router (the supervisor brain substrate) or opts.brain (tests)', + ) + } + + const profile = authoringSupervisorProfile(opts.model, opts.supervisor) + + return supervise(profile, intent, { + budget: opts.budget ?? defaultDelegateBudget, + ...(opts.backend ? { backend: opts.backend } : {}), + ...(opts.deliverable ? { deliverable: opts.deliverable as DeliverableSpec } : {}), + ...(opts.router ? { router: opts.router } : {}), + ...(opts.brain ? { brain: opts.brain } : {}), + ...(opts.allowedModels ? { allowedModels: opts.allowedModels } : {}), + ...(opts.runId ? { runId: opts.runId } : {}), + }) as Promise> +} diff --git a/tests/loops/delegate.test.ts b/tests/loops/delegate.test.ts new file mode 100644 index 00000000..65819203 --- /dev/null +++ b/tests/loops/delegate.test.ts @@ -0,0 +1,156 @@ +import { beforeEach, describe, expect, it, vi } from 'vitest' + +// Mock the ONE front door so these unit tests prove delegate's CONTRACT — it routes the intent to +// `supervise()` with a DEFAULT authoring-supervisor profile and returns supervise()'s result +// (including `spentTotal`) UNCHANGED — without standing up a real supervised run / network. The +// offline end-to-end supervise path is covered by supervise.test.ts / supervise-convenience.test.ts. +const superviseSpy = + vi.fn< + ( + profile: unknown, + task: unknown, + opts: unknown, + ) => Promise> + >() +vi.mock('../../src/runtime/supervise/supervise', () => ({ + supervise: (profile: unknown, task: unknown, opts: unknown) => superviseSpy(profile, task, opts), +})) + +import { supervisorInstructions } from '../../src/runtime/supervise/authoring' +import { defaultDelegateBudget, delegate } from '../../src/runtime/supervise/delegate' +import type { ExecutorConfig } from '../../src/runtime/supervise/runtime' +import type { + RouterConfig, + Spend, + SupervisedResult, + TreeView, +} from '../../src/runtime/supervise/types' + +const router: RouterConfig = { + routerBaseUrl: 'http://localhost/v1', + routerKey: 'k', + model: 'deepseek-v4-flash', +} +const backend: ExecutorConfig = { + backend: 'router-tools', + routerBaseUrl: 'http://localhost/v1', + routerKey: 'k', + model: 'deepseek-v4-flash', +} as ExecutorConfig + +const emptyTree = { id: 'root', children: [] } as unknown as TreeView + +const spentTotal: Spend = { + iterations: 3, + tokens: { input: 1200, output: 800 }, + usd: 0.0042, + ms: 5100, +} + +function winner(out: unknown): SupervisedResult { + return { kind: 'winner', out, outRef: 'blob:1', tree: emptyTree, spentTotal } +} + +beforeEach(() => { + superviseSpy.mockReset() + superviseSpy.mockResolvedValue(winner({ answer: 42 })) +}) + +describe('delegate — the one generic delegation verb over supervise()', () => { + it('routes to supervise() with the DEFAULT authoring-supervisor profile (no hardcoded worker)', async () => { + await delegate('fix the failing auth test', { backend, router }) + + expect(superviseSpy).toHaveBeenCalledTimes(1) + const [profile, task, opts] = superviseSpy.mock.calls[0] as [ + { name?: string; harness?: unknown; systemPrompt?: string }, + unknown, + { backend?: unknown; router?: unknown; budget?: unknown }, + ] + // A router-brained AUTHORING supervisor: its standing instruction IS the authoring skill, so it + // writes its own worker profile from the intent — no worker profile is baked into delegate. + expect(profile.harness ?? null).toBeNull() + expect(profile.systemPrompt).toBe(supervisorInstructions()) + // The intent is handed through verbatim as the task. + expect(task).toBe('fix the failing auth test') + // The injected substrate (where workers run + the brain) is forwarded. + expect(opts.backend).toBe(backend) + expect(opts.router).toBe(router) + expect(opts.budget).toBe(defaultDelegateBudget) + }) + + it('returns the supervise() result UNCHANGED so spentTotal (the cost) rides through', async () => { + const canned = winner({ patch: 'diff' }) + superviseSpy.mockResolvedValue(canned) + + const result = await delegate('refactor the parser', { backend, router }) + + expect(result).toBe(canned) + expect(result.kind).toBe('winner') + if (result.kind === 'winner') { + // The whole reason delegate beats delegate_code: the real spend comes back. + expect(result.spentTotal).toEqual(spentTotal) + expect(result.spentTotal.usd).toBeGreaterThan(0) + expect(result.spentTotal.tokens.input).toBe(1200) + } + }) + + it('forwards a no-winner result faithfully (never fabricates a success)', async () => { + const noWinner: SupervisedResult = { + kind: 'no-winner', + reason: 'budget-exhausted', + tree: emptyTree, + downCount: 1, + } + superviseSpy.mockResolvedValue(noWinner) + + const result = await delegate('do the thing', { backend, router }) + expect(result.kind).toBe('no-winner') + }) + + it('forwards deliverable, model, budget, allowedModels, runId to supervise()', async () => { + const deliverable = { check: () => true, describe: 'always delivered' } + const budget = { maxIterations: 7, maxTokens: 9000 } + + await delegate('intent', { + backend, + router, + deliverable, + model: 'glm-5.2', + budget, + allowedModels: ['glm-5.2', 'deepseek-v4-flash'], + runId: 'run-7', + }) + + const [profile, , opts] = superviseSpy.mock.calls[0] as [ + { model?: string }, + unknown, + Record, + ] + expect(profile.model).toBe('glm-5.2') + expect(opts.deliverable).toBe(deliverable) + expect(opts.budget).toBe(budget) + expect(opts.allowedModels).toEqual(['glm-5.2', 'deepseek-v4-flash']) + expect(opts.runId).toBe('run-7') + }) + + it('lets the caller override only the supervisor name/stance', async () => { + await delegate('intent', { + backend, + router, + supervisor: { name: 'my-supervisor', systemPrompt: 'custom stance' }, + }) + const [profile] = superviseSpy.mock.calls[0] as [{ name?: string; systemPrompt?: string }] + expect(profile.name).toBe('my-supervisor') + expect(profile.systemPrompt).toBe('custom stance') + }) + + it('fails loud on an empty intent', async () => { + await expect(delegate(' ', { backend, router })).rejects.toThrow(/intent/) + expect(superviseSpy).not.toHaveBeenCalled() + }) + + it('fails loud when neither router nor brain is provided (no supervisor brain)', async () => { + await expect(delegate('intent', { backend })).rejects.toThrow(/router|brain/) + expect(superviseSpy).not.toHaveBeenCalled() + }) +}) diff --git a/tests/mcp/delegate.test.ts b/tests/mcp/delegate.test.ts new file mode 100644 index 00000000..eecadf91 --- /dev/null +++ b/tests/mcp/delegate.test.ts @@ -0,0 +1,116 @@ +import { beforeEach, describe, expect, it, vi } from 'vitest' + +// The `delegate` tool routes through `delegate()` → `supervise()`. Mock the ONE front door so the +// tool's contract is provable offline: it hands the agent's INTENT to a supervisor and returns the +// delivered output TOGETHER WITH the conserved cost (spentTotal) — the channel delegate_code lacks. +const superviseSpy = + vi.fn< + ( + profile: unknown, + task: unknown, + opts: unknown, + ) => Promise> + >() +vi.mock('../../src/runtime/supervise/supervise', () => ({ + supervise: (profile: unknown, task: unknown, opts: unknown) => superviseSpy(profile, task, opts), +})) + +import { createMcpServer } from '../../src/mcp/server' +import { + createDelegateHandler, + DELEGATE_TOOL_NAME, + validateDelegateArgs, +} from '../../src/mcp/tools/delegate' +import type { ExecutorConfig } from '../../src/runtime/supervise/runtime' +import type { + RouterConfig, + Spend, + SupervisedResult, + TreeView, +} from '../../src/runtime/supervise/types' + +const router: RouterConfig = { + routerBaseUrl: 'http://localhost/v1', + routerKey: 'k', + model: 'deepseek-v4-flash', +} +const backend: ExecutorConfig = { + backend: 'router-tools', + routerBaseUrl: 'http://localhost/v1', + routerKey: 'k', + model: 'deepseek-v4-flash', +} as ExecutorConfig + +const emptyTree = { id: 'root', children: [] } as unknown as TreeView +const spentTotal: Spend = { + iterations: 2, + tokens: { input: 500, output: 300 }, + usd: 0.0019, + ms: 4200, +} +const winnerResult: SupervisedResult = { + kind: 'winner', + out: { patch: 'diff' }, + outRef: 'blob:1', + tree: emptyTree, + spentTotal, +} + +beforeEach(() => { + superviseSpy.mockReset() + superviseSpy.mockResolvedValue(winnerResult) +}) + +describe('delegate MCP tool — generic delegation verb that returns cost', () => { + it('validates a non-empty intent', () => { + expect(() => validateDelegateArgs({})).toThrow(/intent/) + expect(() => validateDelegateArgs({ intent: ' ' })).toThrow(/intent/) + expect(validateDelegateArgs({ intent: 'do x' }).intent).toBe('do x') + }) + + it('routes the intent to delegate()/supervise() and returns the delivered output WITH spentTotal', async () => { + const handler = createDelegateHandler({ router, backend }) + const result = (await handler({ intent: 'fix the bug' })) as { + status: string + out: unknown + spentTotal: Spend + } + + expect(superviseSpy).toHaveBeenCalledTimes(1) + const [, task] = superviseSpy.mock.calls[0] as [unknown, unknown, unknown] + expect(task).toBe('fix the bug') + + expect(result.status).toBe('winner') + expect(result.out).toEqual({ patch: 'diff' }) + // The whole point: the agent learns what the delegation cost. + expect(result.spentTotal).toEqual(spentTotal) + }) + + it('reports no-winner faithfully with the reason (never a faked success)', async () => { + superviseSpy.mockResolvedValue({ + kind: 'no-winner', + reason: 'all-children-down', + tree: emptyTree, + downCount: 2, + }) + const handler = createDelegateHandler({ router, backend }) + const result = (await handler({ intent: 'do x' })) as { status: string; reason: string } + expect(result.status).toBe('no-winner') + expect(result.reason).toBe('all-children-down') + }) + + it('applies a per-call model override', async () => { + const handler = createDelegateHandler({ router, backend, model: 'deepseek-v4-flash' }) + await handler({ intent: 'do x', model: 'glm-5.2' }) + const [profile] = superviseSpy.mock.calls[0] as [{ model?: string }] + expect(profile.model).toBe('glm-5.2') + }) + + it('createMcpServer registers `delegate` only when delegateSupervisor is wired', () => { + const without = createMcpServer({}) + expect(without.tools.has(DELEGATE_TOOL_NAME)).toBe(false) + + const withSupervisor = createMcpServer({ delegateSupervisor: { router, backend } }) + expect(withSupervisor.tools.has(DELEGATE_TOOL_NAME)).toBe(true) + }) +}) From bad18b92af0a6169d1a5fefbea95cb7d348b9d82 Mon Sep 17 00:00:00 2001 From: Drew Stone Date: Mon, 22 Jun 2026 02:14:36 -0600 Subject: [PATCH 2/2] fix(delegate): no-winner carries real spentTotal + docs freshness (version pin + platform entryPoint) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The no-winner variant of SupervisedResult lacked a spentTotal field even though real conserved compute is spent before a run fails — so a failed delegation reported its cost as absent (and the delegate MCP tool fabricated a zero). Add spentTotal as a required field on the no-winner variant and compute it off the same journal the winner path reads (spentFromJournal), DRY'd into one noWinner() builder over the two no-winner exit points. The delegate MCP tool now returns the real result.spentTotal on no-winner; the fabricated zeroSpend constant is removed. This is delegate()'s whole point: the cost rides back on BOTH paths. Docs freshness gate (pnpm docs:check) was red on main from #352: the canonical-api version pin read 0.70.0 while package.json is 0.70.1, and the restored ./platform export had no typedoc entryPoint. Bump the pin to 0.70.1, register src/platform/index.ts in typedoc.json, regenerate docs/api/ (adds platform.md + the new delegate symbols). Add examples/delegate/e2e-delegate-real.ts: a router-brained supervisor authors + spawns a worker (no hardcoded coder profile), the worker does real filesystem work, the deliverable gate reads disk, and result.spentTotal carries the real conserved cost on both winner and no-winner paths. --- docs/api/README.md | 1 + docs/api/index.md | 64 +- docs/api/mcp.md | 2 +- docs/api/platform.md | 1080 ++++++++++++++++++++++++ docs/api/runtime.md | 58 +- docs/canonical-api.md | 2 +- examples/delegate/e2e-delegate-real.ts | 128 +++ src/mcp/tools/delegate.ts | 9 +- src/runtime/supervise/delegate.ts | 5 +- src/runtime/supervise/supervisor.ts | 25 +- src/runtime/supervise/types.ts | 4 + tests/loops/delegate.test.ts | 7 +- tests/loops/supervise.test.ts | 7 + tests/mcp/delegate.test.ts | 11 +- typedoc.json | 1 + 15 files changed, 1332 insertions(+), 72 deletions(-) create mode 100644 docs/api/platform.md create mode 100644 examples/delegate/e2e-delegate-real.ts diff --git a/docs/api/README.md b/docs/api/README.md index 64bf07e2..929569ed 100644 --- a/docs/api/README.md +++ b/docs/api/README.md @@ -11,5 +11,6 @@ - [index](index.md) - [intelligence](intelligence.md) - [mcp](mcp.md) +- [platform](platform.md) - [profiles](profiles.md) - [runtime](runtime.md) diff --git a/docs/api/index.md b/docs/api/index.md index 8e422a1d..25977f68 100644 --- a/docs/api/index.md +++ b/docs/api/index.md @@ -3824,7 +3824,7 @@ True when the iteration carried an error — maps to OTEL status code 2. ### EvalRunGeneration -Defined in: [otel-export.ts:521](https://github.com/tangle-network/agent-runtime/blob/main/src/otel-export.ts#L521) +Defined in: [otel-export.ts:529](https://github.com/tangle-network/agent-runtime/blob/main/src/otel-export.ts#L529) #### Properties @@ -3832,7 +3832,7 @@ Defined in: [otel-export.ts:521](https://github.com/tangle-network/agent-runtime > **index**: `number` -Defined in: [otel-export.ts:523](https://github.com/tangle-network/agent-runtime/blob/main/src/otel-export.ts#L523) +Defined in: [otel-export.ts:531](https://github.com/tangle-network/agent-runtime/blob/main/src/otel-export.ts#L531) 0-based ordinal of this generation within the run (required by ingest). @@ -3840,7 +3840,7 @@ Defined in: [otel-export.ts:523](https://github.com/tangle-network/agent-runtime > **surfaceHash**: `string` -Defined in: [otel-export.ts:525](https://github.com/tangle-network/agent-runtime/blob/main/src/otel-export.ts#L525) +Defined in: [otel-export.ts:533](https://github.com/tangle-network/agent-runtime/blob/main/src/otel-export.ts#L533) Identity of the proposed surface change (content-addressed hash). @@ -3848,7 +3848,7 @@ Identity of the proposed surface change (content-addressed hash). > `optional` **surface?**: `unknown` -Defined in: [otel-export.ts:527](https://github.com/tangle-network/agent-runtime/blob/main/src/otel-export.ts#L527) +Defined in: [otel-export.ts:535](https://github.com/tangle-network/agent-runtime/blob/main/src/otel-export.ts#L535) Arbitrary provenance for this generation (rationale, evidence, source). @@ -3856,7 +3856,7 @@ Arbitrary provenance for this generation (rationale, evidence, source). > `optional` **cells?**: `unknown`[] -Defined in: [otel-export.ts:529](https://github.com/tangle-network/agent-runtime/blob/main/src/otel-export.ts#L529) +Defined in: [otel-export.ts:537](https://github.com/tangle-network/agent-runtime/blob/main/src/otel-export.ts#L537) Per-scenario results; empty until the generation is measured. @@ -3864,7 +3864,7 @@ Per-scenario results; empty until the generation is measured. > **compositeMean**: `number` -Defined in: [otel-export.ts:531](https://github.com/tangle-network/agent-runtime/blob/main/src/otel-export.ts#L531) +Defined in: [otel-export.ts:539](https://github.com/tangle-network/agent-runtime/blob/main/src/otel-export.ts#L539) Mean composite score (0 when unmeasured — pair with labels.measured). @@ -3872,19 +3872,19 @@ Mean composite score (0 when unmeasured — pair with labels.measured). > **costUsd**: `number` -Defined in: [otel-export.ts:532](https://github.com/tangle-network/agent-runtime/blob/main/src/otel-export.ts#L532) +Defined in: [otel-export.ts:540](https://github.com/tangle-network/agent-runtime/blob/main/src/otel-export.ts#L540) ##### durationMs > **durationMs**: `number` -Defined in: [otel-export.ts:533](https://github.com/tangle-network/agent-runtime/blob/main/src/otel-export.ts#L533) +Defined in: [otel-export.ts:541](https://github.com/tangle-network/agent-runtime/blob/main/src/otel-export.ts#L541) *** ### EvalRunEvent -Defined in: [otel-export.ts:536](https://github.com/tangle-network/agent-runtime/blob/main/src/otel-export.ts#L536) +Defined in: [otel-export.ts:544](https://github.com/tangle-network/agent-runtime/blob/main/src/otel-export.ts#L544) #### Properties @@ -3892,19 +3892,19 @@ Defined in: [otel-export.ts:536](https://github.com/tangle-network/agent-runtime > **runId**: `string` -Defined in: [otel-export.ts:537](https://github.com/tangle-network/agent-runtime/blob/main/src/otel-export.ts#L537) +Defined in: [otel-export.ts:545](https://github.com/tangle-network/agent-runtime/blob/main/src/otel-export.ts#L545) ##### runDir > **runDir**: `string` -Defined in: [otel-export.ts:538](https://github.com/tangle-network/agent-runtime/blob/main/src/otel-export.ts#L538) +Defined in: [otel-export.ts:546](https://github.com/tangle-network/agent-runtime/blob/main/src/otel-export.ts#L546) ##### timestamp > **timestamp**: `string` -Defined in: [otel-export.ts:540](https://github.com/tangle-network/agent-runtime/blob/main/src/otel-export.ts#L540) +Defined in: [otel-export.ts:548](https://github.com/tangle-network/agent-runtime/blob/main/src/otel-export.ts#L548) ISO timestamp. @@ -3912,61 +3912,61 @@ ISO timestamp. > **status**: `"started"` \| `"baseline-complete"` \| `"generation-complete"` \| `"gate-decided"` \| `"finished"` \| `"errored"` -Defined in: [otel-export.ts:541](https://github.com/tangle-network/agent-runtime/blob/main/src/otel-export.ts#L541) +Defined in: [otel-export.ts:549](https://github.com/tangle-network/agent-runtime/blob/main/src/otel-export.ts#L549) ##### labels? > `optional` **labels?**: `Record`\<`string`, `string`\> -Defined in: [otel-export.ts:548](https://github.com/tangle-network/agent-runtime/blob/main/src/otel-export.ts#L548) +Defined in: [otel-export.ts:556](https://github.com/tangle-network/agent-runtime/blob/main/src/otel-export.ts#L556) ##### baseline? > `optional` **baseline?**: [`EvalRunGeneration`](#evalrungeneration) -Defined in: [otel-export.ts:549](https://github.com/tangle-network/agent-runtime/blob/main/src/otel-export.ts#L549) +Defined in: [otel-export.ts:557](https://github.com/tangle-network/agent-runtime/blob/main/src/otel-export.ts#L557) ##### generations? > `optional` **generations?**: [`EvalRunGeneration`](#evalrungeneration)[] -Defined in: [otel-export.ts:550](https://github.com/tangle-network/agent-runtime/blob/main/src/otel-export.ts#L550) +Defined in: [otel-export.ts:558](https://github.com/tangle-network/agent-runtime/blob/main/src/otel-export.ts#L558) ##### gateDecision? > `optional` **gateDecision?**: `"ship"` \| `"hold"` \| `"need_more_work"` \| `"model_ceiling"` \| `"arch_ceiling"` -Defined in: [otel-export.ts:551](https://github.com/tangle-network/agent-runtime/blob/main/src/otel-export.ts#L551) +Defined in: [otel-export.ts:559](https://github.com/tangle-network/agent-runtime/blob/main/src/otel-export.ts#L559) ##### holdoutLift? > `optional` **holdoutLift?**: `number` -Defined in: [otel-export.ts:552](https://github.com/tangle-network/agent-runtime/blob/main/src/otel-export.ts#L552) +Defined in: [otel-export.ts:560](https://github.com/tangle-network/agent-runtime/blob/main/src/otel-export.ts#L560) ##### totalCostUsd > **totalCostUsd**: `number` -Defined in: [otel-export.ts:553](https://github.com/tangle-network/agent-runtime/blob/main/src/otel-export.ts#L553) +Defined in: [otel-export.ts:561](https://github.com/tangle-network/agent-runtime/blob/main/src/otel-export.ts#L561) ##### totalDurationMs > **totalDurationMs**: `number` -Defined in: [otel-export.ts:554](https://github.com/tangle-network/agent-runtime/blob/main/src/otel-export.ts#L554) +Defined in: [otel-export.ts:562](https://github.com/tangle-network/agent-runtime/blob/main/src/otel-export.ts#L562) ##### errorMessage? > `optional` **errorMessage?**: `string` -Defined in: [otel-export.ts:555](https://github.com/tangle-network/agent-runtime/blob/main/src/otel-export.ts#L555) +Defined in: [otel-export.ts:563](https://github.com/tangle-network/agent-runtime/blob/main/src/otel-export.ts#L563) *** ### EvalRunsExportConfig -Defined in: [otel-export.ts:558](https://github.com/tangle-network/agent-runtime/blob/main/src/otel-export.ts#L558) +Defined in: [otel-export.ts:566](https://github.com/tangle-network/agent-runtime/blob/main/src/otel-export.ts#L566) #### Properties @@ -3974,7 +3974,7 @@ Defined in: [otel-export.ts:558](https://github.com/tangle-network/agent-runtime > `optional` **apiKey?**: `string` -Defined in: [otel-export.ts:560](https://github.com/tangle-network/agent-runtime/blob/main/src/otel-export.ts#L560) +Defined in: [otel-export.ts:568](https://github.com/tangle-network/agent-runtime/blob/main/src/otel-export.ts#L568) Bearer key — tenant is resolved server-side from it. Reads TANGLE_API_KEY. @@ -3982,7 +3982,7 @@ Bearer key — tenant is resolved server-side from it. Reads TANGLE_API_KEY. > `optional` **base?**: `string` -Defined in: [otel-export.ts:562](https://github.com/tangle-network/agent-runtime/blob/main/src/otel-export.ts#L562) +Defined in: [otel-export.ts:570](https://github.com/tangle-network/agent-runtime/blob/main/src/otel-export.ts#L570) Intelligence base. Reads INTELLIGENCE_BASE env, else prod. @@ -3990,7 +3990,7 @@ Intelligence base. Reads INTELLIGENCE_BASE env, else prod. > `optional` **idempotencyKey?**: `string` -Defined in: [otel-export.ts:564](https://github.com/tangle-network/agent-runtime/blob/main/src/otel-export.ts#L564) +Defined in: [otel-export.ts:572](https://github.com/tangle-network/agent-runtime/blob/main/src/otel-export.ts#L572) Idempotency-Key header (e.g. the runId) — safe retries + upsert. @@ -3998,7 +3998,7 @@ Idempotency-Key header (e.g. the runId) — safe retries + upsert. ### EvalRunsExportResult -Defined in: [otel-export.ts:567](https://github.com/tangle-network/agent-runtime/blob/main/src/otel-export.ts#L567) +Defined in: [otel-export.ts:575](https://github.com/tangle-network/agent-runtime/blob/main/src/otel-export.ts#L575) #### Properties @@ -4006,25 +4006,25 @@ Defined in: [otel-export.ts:567](https://github.com/tangle-network/agent-runtime > **ok**: `boolean` -Defined in: [otel-export.ts:568](https://github.com/tangle-network/agent-runtime/blob/main/src/otel-export.ts#L568) +Defined in: [otel-export.ts:576](https://github.com/tangle-network/agent-runtime/blob/main/src/otel-export.ts#L576) ##### status > **status**: `number` -Defined in: [otel-export.ts:569](https://github.com/tangle-network/agent-runtime/blob/main/src/otel-export.ts#L569) +Defined in: [otel-export.ts:577](https://github.com/tangle-network/agent-runtime/blob/main/src/otel-export.ts#L577) ##### accepted > **accepted**: `number` -Defined in: [otel-export.ts:570](https://github.com/tangle-network/agent-runtime/blob/main/src/otel-export.ts#L570) +Defined in: [otel-export.ts:578](https://github.com/tangle-network/agent-runtime/blob/main/src/otel-export.ts#L578) ##### rejected > **rejected**: `object`[] -Defined in: [otel-export.ts:571](https://github.com/tangle-network/agent-runtime/blob/main/src/otel-export.ts#L571) +Defined in: [otel-export.ts:579](https://github.com/tangle-network/agent-runtime/blob/main/src/otel-export.ts#L579) ###### index @@ -7032,7 +7032,7 @@ Defined in: [model-resolution.ts:41](https://github.com/tangle-network/agent-run > `const` **INTELLIGENCE\_WIRE\_VERSION**: `"2026-05-26.v1"` = `'2026-05-26.v1'` -Defined in: [otel-export.ts:519](https://github.com/tangle-network/agent-runtime/blob/main/src/otel-export.ts#L519) +Defined in: [otel-export.ts:527](https://github.com/tangle-network/agent-runtime/blob/main/src/otel-export.ts#L527) Wire version the eval-runs ingest enforces (X-Tangle-Wire-Version + body). @@ -8446,7 +8446,7 @@ readonly `object`[] > **exportEvalRuns**(`events`, `config?`): `Promise`\<[`EvalRunsExportResult`](#evalrunsexportresult)\> -Defined in: [otel-export.ts:582](https://github.com/tangle-network/agent-runtime/blob/main/src/otel-export.ts#L582) +Defined in: [otel-export.ts:590](https://github.com/tangle-network/agent-runtime/blob/main/src/otel-export.ts#L590) Ship self-improvement eval-run events to Tangle Intelligence. Unlike the best-effort span exporter, this RESOLVES with the ingest verdict (accepted / diff --git a/docs/api/mcp.md b/docs/api/mcp.md index a9cfdbbe..88d50ad7 100644 --- a/docs/api/mcp.md +++ b/docs/api/mcp.md @@ -8208,7 +8208,7 @@ Defined in: [mcp/tools/delegate.ts:78](https://github.com/tangle-network/agent-r > **createDelegateHandler**(`options`): (`raw`) => `Promise`\<[`DelegateResult`](#delegateresult)\> -Defined in: [mcp/tools/delegate.ts:140](https://github.com/tangle-network/agent-runtime/blob/main/src/mcp/tools/delegate.ts#L140) +Defined in: [mcp/tools/delegate.ts:139](https://github.com/tangle-network/agent-runtime/blob/main/src/mcp/tools/delegate.ts#L139) Build the `delegate` tool handler. Closes over the injected supervisor substrate (`router` / `backend` / `deliverable`); each call routes the agent's intent to `delegate()` and returns the diff --git a/docs/api/platform.md b/docs/api/platform.md new file mode 100644 index 00000000..72e180e3 --- /dev/null +++ b/docs/api/platform.md @@ -0,0 +1,1080 @@ +[**@tangle-network/agent-runtime**](README.md) + +*** + +[@tangle-network/agent-runtime](README.md) / platform + +# platform + +## Classes + +### PlatformAuthError + +Defined in: [platform/auth.ts:50](https://github.com/tangle-network/agent-runtime/blob/main/src/platform/auth.ts#L50) + +`@tangle-network/agent-runtime/platform` — typed server-side clients +for the Tangle platform's cross-site SSO bridge and integrations +hub. Apps consume these to avoid rolling their own OAuth, session, +and connection storage. + +See: + - [PlatformAuthClient](#platformauthclient) for "Login with Tangle" + - [PlatformHubClient](#platformhubclient) for the `/v1/hub/*` surface + +#### Extends + +- `Error` + +#### Constructors + +##### Constructor + +> **new PlatformAuthError**(`message`, `status`, `body`): [`PlatformAuthError`](#platformautherror) + +Defined in: [platform/auth.ts:51](https://github.com/tangle-network/agent-runtime/blob/main/src/platform/auth.ts#L51) + +###### Parameters + +###### message + +`string` + +###### status + +`number` + +###### body + +`unknown` + +###### Returns + +[`PlatformAuthError`](#platformautherror) + +###### Overrides + +`Error.constructor` + +#### Properties + +##### status + +> `readonly` **status**: `number` + +Defined in: [platform/auth.ts:53](https://github.com/tangle-network/agent-runtime/blob/main/src/platform/auth.ts#L53) + +##### body + +> `readonly` **body**: `unknown` + +Defined in: [platform/auth.ts:54](https://github.com/tangle-network/agent-runtime/blob/main/src/platform/auth.ts#L54) + +*** + +### PlatformAuthClient + +Defined in: [platform/auth.ts:61](https://github.com/tangle-network/agent-runtime/blob/main/src/platform/auth.ts#L61) + +`@tangle-network/agent-runtime/platform` — typed server-side clients +for the Tangle platform's cross-site SSO bridge and integrations +hub. Apps consume these to avoid rolling their own OAuth, session, +and connection storage. + +See: + - [PlatformAuthClient](#platformauthclient) for "Login with Tangle" + - [PlatformHubClient](#platformhubclient) for the `/v1/hub/*` surface + +#### Constructors + +##### Constructor + +> **new PlatformAuthClient**(`options`): [`PlatformAuthClient`](#platformauthclient) + +Defined in: [platform/auth.ts:66](https://github.com/tangle-network/agent-runtime/blob/main/src/platform/auth.ts#L66) + +###### Parameters + +###### options + +[`PlatformAuthClientOptions`](#platformauthclientoptions) + +###### Returns + +[`PlatformAuthClient`](#platformauthclient) + +#### Methods + +##### authorizeUrl() + +> **authorizeUrl**(`options`): `string` + +Defined in: [platform/auth.ts:81](https://github.com/tangle-network/agent-runtime/blob/main/src/platform/auth.ts#L81) + +Build the URL the user is redirected to in order to start SSO. +The platform redirects back to one of `appId`'s registered +`redirectUris` with `?code=...&app=...&state=...`. + +###### Parameters + +###### options + +[`AuthorizeUrlOptions`](#authorizeurloptions) + +###### Returns + +`string` + +##### exchange() + +> **exchange**(`code`): `Promise`\<[`ExchangeCodeResult`](#exchangecoderesult)\> + +Defined in: [platform/auth.ts:99](https://github.com/tangle-network/agent-runtime/blob/main/src/platform/auth.ts#L99) + +Exchange a single-use auth code (delivered to the consumer's +callback by the platform) for an API key + the user's identity. +Codes are single-use and expire ~5 minutes after issue. + +###### Parameters + +###### code + +`string` + +###### Returns + +`Promise`\<[`ExchangeCodeResult`](#exchangecoderesult)\> + +*** + +### PlatformHubError + +Defined in: [platform/integrations.ts:132](https://github.com/tangle-network/agent-runtime/blob/main/src/platform/integrations.ts#L132) + +#### Extends + +- `Error` + +#### Constructors + +##### Constructor + +> **new PlatformHubError**(`message`, `status`, `code`, `body`): [`PlatformHubError`](#platformhuberror) + +Defined in: [platform/integrations.ts:133](https://github.com/tangle-network/agent-runtime/blob/main/src/platform/integrations.ts#L133) + +###### Parameters + +###### message + +`string` + +###### status + +`number` + +###### code + +`string` \| `undefined` + +###### body + +`unknown` + +###### Returns + +[`PlatformHubError`](#platformhuberror) + +###### Overrides + +`Error.constructor` + +#### Properties + +##### status + +> `readonly` **status**: `number` + +Defined in: [platform/integrations.ts:135](https://github.com/tangle-network/agent-runtime/blob/main/src/platform/integrations.ts#L135) + +##### code + +> `readonly` **code**: `string` \| `undefined` + +Defined in: [platform/integrations.ts:136](https://github.com/tangle-network/agent-runtime/blob/main/src/platform/integrations.ts#L136) + +##### body + +> `readonly` **body**: `unknown` + +Defined in: [platform/integrations.ts:137](https://github.com/tangle-network/agent-runtime/blob/main/src/platform/integrations.ts#L137) + +*** + +### PlatformHubClient + +Defined in: [platform/integrations.ts:150](https://github.com/tangle-network/agent-runtime/blob/main/src/platform/integrations.ts#L150) + +#### Constructors + +##### Constructor + +> **new PlatformHubClient**(`options`): [`PlatformHubClient`](#platformhubclient) + +Defined in: [platform/integrations.ts:155](https://github.com/tangle-network/agent-runtime/blob/main/src/platform/integrations.ts#L155) + +###### Parameters + +###### options + +[`PlatformHubClientOptions`](#platformhubclientoptions) + +###### Returns + +[`PlatformHubClient`](#platformhubclient) + +#### Methods + +##### catalog() + +> **catalog**(): `Promise`\<[`CatalogResult`](#catalogresult)\> + +Defined in: [platform/integrations.ts:166](https://github.com/tangle-network/agent-runtime/blob/main/src/platform/integrations.ts#L166) + +GET /v1/hub/providers — the connectable provider catalog. + +###### Returns + +`Promise`\<[`CatalogResult`](#catalogresult)\> + +##### listConnections() + +> **listConnections**(): `Promise`\<[`PlatformConnection`](#platformconnection)[]\> + +Defined in: [platform/integrations.ts:171](https://github.com/tangle-network/agent-runtime/blob/main/src/platform/integrations.ts#L171) + +GET /v1/hub/connections — the calling user's live connections. + +###### Returns + +`Promise`\<[`PlatformConnection`](#platformconnection)[]\> + +##### revokeConnection() + +> **revokeConnection**(`connectionId`): `Promise`\<\{ `connection`: [`PlatformConnection`](#platformconnection); \}\> + +Defined in: [platform/integrations.ts:180](https://github.com/tangle-network/agent-runtime/blob/main/src/platform/integrations.ts#L180) + +DELETE /v1/hub/connections/:connectionId — revoke + disable a connection. + +###### Parameters + +###### connectionId + +`string` + +###### Returns + +`Promise`\<\{ `connection`: [`PlatformConnection`](#platformconnection); \}\> + +##### startAuth() + +> **startAuth**(`input`): `Promise`\<[`StartAuthResult`](#startauthresult)\> + +Defined in: [platform/integrations.ts:190](https://github.com/tangle-network/agent-runtime/blob/main/src/platform/integrations.ts#L190) + +POST /v1/hub/connections/:provider/start — begin OAuth/grant. The provider +is taken from the URL; the body carries `returnUrl` (+ `cli`). The platform's +two start branches name the URL field differently (github → `authorizationUrl`, +substrate → `redirectUrl`); this normalizes to `authorizationUrl`. + +###### Parameters + +###### input + +[`StartAuthInput`](#startauthinput) + +###### Returns + +`Promise`\<[`StartAuthResult`](#startauthresult)\> + +##### listHealthchecks() + +> **listHealthchecks**(): `Promise`\<[`HealthCheck`](#healthcheck)[]\> + +Defined in: [platform/integrations.ts:217](https://github.com/tangle-network/agent-runtime/blob/main/src/platform/integrations.ts#L217) + +Last-known health for every connection. The platform has no global +healthcheck listing — health rides on each connection row — so this derives +the list from `listConnections()` (one request, no extra round-trips). + +###### Returns + +`Promise`\<[`HealthCheck`](#healthcheck)[]\> + +##### checkConnectionHealth() + +> **checkConnectionHealth**(`connectionId`): `Promise`\<[`ConnectionHealthResult`](#connectionhealthresult)\> + +Defined in: [platform/integrations.ts:231](https://github.com/tangle-network/agent-runtime/blob/main/src/platform/integrations.ts#L231) + +POST /v1/hub/connections/:connectionId/health — trigger a fresh health +probe for one connection and return its updated state. + +###### Parameters + +###### connectionId + +`string` + +###### Returns + +`Promise`\<[`ConnectionHealthResult`](#connectionhealthresult)\> + +##### runHealthchecks() + +> **runHealthchecks**(): `Promise`\<\{ `scheduled`: `number`; \}\> + +Defined in: [platform/integrations.ts:240](https://github.com/tangle-network/agent-runtime/blob/main/src/platform/integrations.ts#L240) + +Trigger a fresh health probe across all of the user's connections. The +platform exposes health per-connection only, so this fans out over +`listConnections()`. `scheduled` is the number of probes dispatched. + +###### Returns + +`Promise`\<\{ `scheduled`: `number`; \}\> + +##### status() + +> **status**(): `Promise`\<[`PlatformHubStatus`](#platformhubstatus)\> + +Defined in: [platform/integrations.ts:247](https://github.com/tangle-network/agent-runtime/blob/main/src/platform/integrations.ts#L247) + +GET /v1/hub/status — principal + aggregate connection counts. + +###### Returns + +`Promise`\<[`PlatformHubStatus`](#platformhubstatus)\> + +##### mintToken() + +> **mintToken**(`input`): `Promise`\<[`MintTokenResult`](#minttokenresult)\> + +Defined in: [platform/integrations.ts:256](https://github.com/tangle-network/agent-runtime/blob/main/src/platform/integrations.ts#L256) + +POST /v1/hub/tokens — mint a short-lived, action-scoped capability token a +sandbox can use to invoke one hub action on the user's behalf without +seeing the underlying provider credential. + +###### Parameters + +###### input + +[`MintTokenInput`](#minttokeninput) + +###### Returns + +`Promise`\<[`MintTokenResult`](#minttokenresult)\> + +##### exec() + +> **exec**(`input`): `Promise`\<`unknown`\> + +Defined in: [platform/integrations.ts:261](https://github.com/tangle-network/agent-runtime/blob/main/src/platform/integrations.ts#L261) + +POST /v1/hub/exec — execute a hub action and return its result. + +###### Parameters + +###### input + +[`ExecInput`](#execinput) + +###### Returns + +`Promise`\<`unknown`\> + +## Interfaces + +### PlatformAuthClientOptions + +Defined in: [platform/auth.ts:15](https://github.com/tangle-network/agent-runtime/blob/main/src/platform/auth.ts#L15) + +Server-side client for the Tangle platform's cross-site SSO bridge. + +Consumer apps (gtm-agent, tax-agent, legal-agent, creative-agent, …) +use this to: + 1. Build an /authorize URL that lands the user on id.tangle.tools + and brings them back with a single-use code. + 2. Exchange that code for an API key + the user's identity. + +The platform endpoint contract is documented in +`products/platform/api/src/routes/cross-site.ts`. This client only +speaks HTTP — no SDK weight, no transitive deps. + +#### Properties + +##### baseUrl + +> **baseUrl**: `string` + +Defined in: [platform/auth.ts:17](https://github.com/tangle-network/agent-runtime/blob/main/src/platform/auth.ts#L17) + +Platform base URL, e.g. `https://id.tangle.tools`. + +##### appId + +> **appId**: `string` + +Defined in: [platform/auth.ts:19](https://github.com/tangle-network/agent-runtime/blob/main/src/platform/auth.ts#L19) + +App id as registered in the platform's TRUSTED_APPS registry. + +##### fetchImpl? + +> `optional` **fetchImpl?**: (`input`, `init?`) => `Promise`\<`Response`\> + +Defined in: [platform/auth.ts:21](https://github.com/tangle-network/agent-runtime/blob/main/src/platform/auth.ts#L21) + +Override the global fetch (useful for tests + edge runtimes). + +###### Parameters + +###### input + +`string` \| `URL` \| `Request` + +###### init? + +`RequestInit` + +###### Returns + +`Promise`\<`Response`\> + +*** + +### AuthorizeUrlOptions + +Defined in: [platform/auth.ts:24](https://github.com/tangle-network/agent-runtime/blob/main/src/platform/auth.ts#L24) + +`@tangle-network/agent-runtime/platform` — typed server-side clients +for the Tangle platform's cross-site SSO bridge and integrations +hub. Apps consume these to avoid rolling their own OAuth, session, +and connection storage. + +See: + - [PlatformAuthClient](#platformauthclient) for "Login with Tangle" + - [PlatformHubClient](#platformhubclient) for the `/v1/hub/*` surface + +#### Properties + +##### state + +> **state**: `string` + +Defined in: [platform/auth.ts:26](https://github.com/tangle-network/agent-runtime/blob/main/src/platform/auth.ts#L26) + +Required CSRF token; the consumer verifies it on the callback. + +##### redirectUri? + +> `optional` **redirectUri?**: `string` + +Defined in: [platform/auth.ts:31](https://github.com/tangle-network/agent-runtime/blob/main/src/platform/auth.ts#L31) + +Final redirect URI. Must be one of the URIs registered for `appId` +on the platform. Omit to use the first registered URI. + +##### prompt? + +> `optional` **prompt?**: `"login"` + +Defined in: [platform/auth.ts:33](https://github.com/tangle-network/agent-runtime/blob/main/src/platform/auth.ts#L33) + +Force the login screen even if a session is already active. + +##### email? + +> `optional` **email?**: `string` + +Defined in: [platform/auth.ts:35](https://github.com/tangle-network/agent-runtime/blob/main/src/platform/auth.ts#L35) + +Pre-fill the email field on the login screen. + +*** + +### ExchangeCodeResult + +Defined in: [platform/auth.ts:38](https://github.com/tangle-network/agent-runtime/blob/main/src/platform/auth.ts#L38) + +`@tangle-network/agent-runtime/platform` — typed server-side clients +for the Tangle platform's cross-site SSO bridge and integrations +hub. Apps consume these to avoid rolling their own OAuth, session, +and connection storage. + +See: + - [PlatformAuthClient](#platformauthclient) for "Login with Tangle" + - [PlatformHubClient](#platformhubclient) for the `/v1/hub/*` surface + +#### Properties + +##### apiKey + +> **apiKey**: `string` + +Defined in: [platform/auth.ts:39](https://github.com/tangle-network/agent-runtime/blob/main/src/platform/auth.ts#L39) + +##### user + +> **user**: `object` + +Defined in: [platform/auth.ts:40](https://github.com/tangle-network/agent-runtime/blob/main/src/platform/auth.ts#L40) + +###### id + +> **id**: `string` + +###### email + +> **email**: `string` + +###### name? + +> `optional` **name?**: `string` + +##### plan + +> **plan**: `object` + +Defined in: [platform/auth.ts:45](https://github.com/tangle-network/agent-runtime/blob/main/src/platform/auth.ts#L45) + +###### tier + +> **tier**: `string` + +*** + +### PlatformHubClientOptions + +Defined in: [platform/integrations.ts:15](https://github.com/tangle-network/agent-runtime/blob/main/src/platform/integrations.ts#L15) + +Server-side client for the Tangle platform's integration hub +(`/v1/hub/*`). Consumer apps use this instead of rolling their own +OAuth + connection tables. + +Auth: the caller supplies a bearer (either the user's API key from +cross-site exchange, or a platform service token) on construction. + +Endpoint contract (authoritative): the platform's `src/lib/hub-contract.ts` ++ `src/routes/hub.ts`. The platform wraps every response in +`{ success, data }`; non-2xx or `success:false` surfaces as `PlatformHubError` +carrying the real upstream status. + +#### Properties + +##### baseUrl + +> **baseUrl**: `string` + +Defined in: [platform/integrations.ts:17](https://github.com/tangle-network/agent-runtime/blob/main/src/platform/integrations.ts#L17) + +Platform base URL, e.g. `https://id.tangle.tools`. + +##### bearer + +> **bearer**: `string` + +Defined in: [platform/integrations.ts:19](https://github.com/tangle-network/agent-runtime/blob/main/src/platform/integrations.ts#L19) + +Bearer credential — user API key or service token. + +##### fetchImpl? + +> `optional` **fetchImpl?**: (`input`, `init?`) => `Promise`\<`Response`\> + +Defined in: [platform/integrations.ts:21](https://github.com/tangle-network/agent-runtime/blob/main/src/platform/integrations.ts#L21) + +Override fetch (tests + edge runtimes). + +###### Parameters + +###### input + +`string` \| `URL` \| `Request` + +###### init? + +`RequestInit` + +###### Returns + +`Promise`\<`Response`\> + +*** + +### PlatformConnection + +Defined in: [platform/integrations.ts:25](https://github.com/tangle-network/agent-runtime/blob/main/src/platform/integrations.ts#L25) + +A live integration connection, as returned by `/v1/hub/connections`. + +#### Properties + +##### id + +> **id**: `string` + +Defined in: [platform/integrations.ts:26](https://github.com/tangle-network/agent-runtime/blob/main/src/platform/integrations.ts#L26) + +##### providerId + +> **providerId**: `string` + +Defined in: [platform/integrations.ts:27](https://github.com/tangle-network/agent-runtime/blob/main/src/platform/integrations.ts#L27) + +##### displayName + +> **displayName**: `string` + +Defined in: [platform/integrations.ts:28](https://github.com/tangle-network/agent-runtime/blob/main/src/platform/integrations.ts#L28) + +##### accountDisplay + +> **accountDisplay**: `string` \| `null` + +Defined in: [platform/integrations.ts:29](https://github.com/tangle-network/agent-runtime/blob/main/src/platform/integrations.ts#L29) + +##### scopes + +> **scopes**: `string`[] + +Defined in: [platform/integrations.ts:30](https://github.com/tangle-network/agent-runtime/blob/main/src/platform/integrations.ts#L30) + +##### status + +> **status**: `string` & `object` \| `"unhealthy"` \| `"active"` \| `"revoked"` \| `"reconnect_required"` + +Defined in: [platform/integrations.ts:31](https://github.com/tangle-network/agent-runtime/blob/main/src/platform/integrations.ts#L31) + +##### health + +> **health**: `string` & `object` \| `"unknown"` \| `"healthy"` \| `"unhealthy"` \| `"rate_limited"` + +Defined in: [platform/integrations.ts:32](https://github.com/tangle-network/agent-runtime/blob/main/src/platform/integrations.ts#L32) + +##### createdAt + +> **createdAt**: `string` + +Defined in: [platform/integrations.ts:33](https://github.com/tangle-network/agent-runtime/blob/main/src/platform/integrations.ts#L33) + +##### updatedAt + +> **updatedAt**: `string` + +Defined in: [platform/integrations.ts:34](https://github.com/tangle-network/agent-runtime/blob/main/src/platform/integrations.ts#L34) + +##### lastUsedAt + +> **lastUsedAt**: `string` \| `null` + +Defined in: [platform/integrations.ts:35](https://github.com/tangle-network/agent-runtime/blob/main/src/platform/integrations.ts#L35) + +*** + +### PlatformCatalogProvider + +Defined in: [platform/integrations.ts:39](https://github.com/tangle-network/agent-runtime/blob/main/src/platform/integrations.ts#L39) + +A connectable provider in the catalog (`/v1/hub/providers`). + +#### Indexable + +> \[`k`: `string`\]: `unknown` + +#### Properties + +##### providerId + +> **providerId**: `string` + +Defined in: [platform/integrations.ts:40](https://github.com/tangle-network/agent-runtime/blob/main/src/platform/integrations.ts#L40) + +##### title? + +> `optional` **title?**: `string` + +Defined in: [platform/integrations.ts:41](https://github.com/tangle-network/agent-runtime/blob/main/src/platform/integrations.ts#L41) + +##### authKind? + +> `optional` **authKind?**: `string` + +Defined in: [platform/integrations.ts:42](https://github.com/tangle-network/agent-runtime/blob/main/src/platform/integrations.ts#L42) + +##### category? + +> `optional` **category?**: `string` + +Defined in: [platform/integrations.ts:43](https://github.com/tangle-network/agent-runtime/blob/main/src/platform/integrations.ts#L43) + +##### scopes? + +> `optional` **scopes?**: `string`[] + +Defined in: [platform/integrations.ts:44](https://github.com/tangle-network/agent-runtime/blob/main/src/platform/integrations.ts#L44) + +##### capabilityCount? + +> `optional` **capabilityCount?**: `number` + +Defined in: [platform/integrations.ts:45](https://github.com/tangle-network/agent-runtime/blob/main/src/platform/integrations.ts#L45) + +##### native? + +> `optional` **native?**: `boolean` + +Defined in: [platform/integrations.ts:46](https://github.com/tangle-network/agent-runtime/blob/main/src/platform/integrations.ts#L46) + +##### configured? + +> `optional` **configured?**: `boolean` + +Defined in: [platform/integrations.ts:49](https://github.com/tangle-network/agent-runtime/blob/main/src/platform/integrations.ts#L49) + +Whether the OAuth app's credentials are wired — the UI offers Connect + only when true. + +*** + +### CatalogResult + +Defined in: [platform/integrations.ts:53](https://github.com/tangle-network/agent-runtime/blob/main/src/platform/integrations.ts#L53) + +#### Indexable + +> \[`k`: `string`\]: `unknown` + +#### Properties + +##### providers + +> **providers**: [`PlatformCatalogProvider`](#platformcatalogprovider)[] + +Defined in: [platform/integrations.ts:54](https://github.com/tangle-network/agent-runtime/blob/main/src/platform/integrations.ts#L54) + +##### substrateBundled? + +> `optional` **substrateBundled?**: `number` + +Defined in: [platform/integrations.ts:56](https://github.com/tangle-network/agent-runtime/blob/main/src/platform/integrations.ts#L56) + +Count of substrate-bundled connectors behind the catalog. + +*** + +### StartAuthInput + +Defined in: [platform/integrations.ts:60](https://github.com/tangle-network/agent-runtime/blob/main/src/platform/integrations.ts#L60) + +#### Properties + +##### providerId + +> **providerId**: `string` + +Defined in: [platform/integrations.ts:62](https://github.com/tangle-network/agent-runtime/blob/main/src/platform/integrations.ts#L62) + +The provider to connect (goes in the URL path). + +##### connectorId? + +> `optional` **connectorId?**: `string` + +Defined in: [platform/integrations.ts:65](https://github.com/tangle-network/agent-runtime/blob/main/src/platform/integrations.ts#L65) + +Accepted for interface compatibility; the platform's start endpoint is + provider-level and does not consume a connector id. + +##### returnUrl + +> **returnUrl**: `string` + +Defined in: [platform/integrations.ts:67](https://github.com/tangle-network/agent-runtime/blob/main/src/platform/integrations.ts#L67) + +Where the platform redirects the user back to after OAuth. + +##### requestedScopes? + +> `optional` **requestedScopes?**: `string`[] + +Defined in: [platform/integrations.ts:69](https://github.com/tangle-network/agent-runtime/blob/main/src/platform/integrations.ts#L69) + +Accepted for interface compatibility; not consumed by the start endpoint. + +##### cli? + +> `optional` **cli?**: `boolean` + +Defined in: [platform/integrations.ts:71](https://github.com/tangle-network/agent-runtime/blob/main/src/platform/integrations.ts#L71) + +CLI flow flag — affects the platform's post-auth redirect handling. + +*** + +### StartAuthResult + +Defined in: [platform/integrations.ts:74](https://github.com/tangle-network/agent-runtime/blob/main/src/platform/integrations.ts#L74) + +#### Properties + +##### authorizationUrl + +> **authorizationUrl**: `string` + +Defined in: [platform/integrations.ts:78](https://github.com/tangle-network/agent-runtime/blob/main/src/platform/integrations.ts#L78) + +The URL to send the user to. Normalized across the platform's two start + branches: github returns `authorizationUrl`, substrate returns + `redirectUrl`. + +##### state + +> **state**: `string` + +Defined in: [platform/integrations.ts:79](https://github.com/tangle-network/agent-runtime/blob/main/src/platform/integrations.ts#L79) + +##### expiresAt? + +> `optional` **expiresAt?**: `string` + +Defined in: [platform/integrations.ts:80](https://github.com/tangle-network/agent-runtime/blob/main/src/platform/integrations.ts#L80) + +##### scopes? + +> `optional` **scopes?**: `string`[] + +Defined in: [platform/integrations.ts:81](https://github.com/tangle-network/agent-runtime/blob/main/src/platform/integrations.ts#L81) + +*** + +### ConnectionHealth + +Defined in: [platform/integrations.ts:84](https://github.com/tangle-network/agent-runtime/blob/main/src/platform/integrations.ts#L84) + +#### Properties + +##### status + +> **status**: `string` & `object` \| `"unknown"` \| `"healthy"` \| `"unhealthy"` \| `"rate_limited"` + +Defined in: [platform/integrations.ts:85](https://github.com/tangle-network/agent-runtime/blob/main/src/platform/integrations.ts#L85) + +##### checkedAt + +> **checkedAt**: `string` + +Defined in: [platform/integrations.ts:86](https://github.com/tangle-network/agent-runtime/blob/main/src/platform/integrations.ts#L86) + +##### error? + +> `optional` **error?**: `object` + +Defined in: [platform/integrations.ts:87](https://github.com/tangle-network/agent-runtime/blob/main/src/platform/integrations.ts#L87) + +###### code + +> **code**: `string` + +###### message + +> **message**: `string` + +*** + +### ConnectionHealthResult + +Defined in: [platform/integrations.ts:90](https://github.com/tangle-network/agent-runtime/blob/main/src/platform/integrations.ts#L90) + +#### Properties + +##### connection + +> **connection**: [`PlatformConnection`](#platformconnection) + +Defined in: [platform/integrations.ts:91](https://github.com/tangle-network/agent-runtime/blob/main/src/platform/integrations.ts#L91) + +##### health + +> **health**: [`ConnectionHealth`](#connectionhealth) + +Defined in: [platform/integrations.ts:92](https://github.com/tangle-network/agent-runtime/blob/main/src/platform/integrations.ts#L92) + +*** + +### HealthCheck + +Defined in: [platform/integrations.ts:96](https://github.com/tangle-network/agent-runtime/blob/main/src/platform/integrations.ts#L96) + +Last-known health for a connection, derived from the connection row. + +#### Properties + +##### connectionId + +> **connectionId**: `string` + +Defined in: [platform/integrations.ts:97](https://github.com/tangle-network/agent-runtime/blob/main/src/platform/integrations.ts#L97) + +##### providerId + +> **providerId**: `string` + +Defined in: [platform/integrations.ts:98](https://github.com/tangle-network/agent-runtime/blob/main/src/platform/integrations.ts#L98) + +##### status + +> **status**: `string` & `object` \| `"unknown"` \| `"healthy"` \| `"unhealthy"` \| `"rate_limited"` + +Defined in: [platform/integrations.ts:100](https://github.com/tangle-network/agent-runtime/blob/main/src/platform/integrations.ts#L100) + +Mirrors `PlatformConnection.health`. + +##### checkedAt? + +> `optional` **checkedAt?**: `string` + +Defined in: [platform/integrations.ts:101](https://github.com/tangle-network/agent-runtime/blob/main/src/platform/integrations.ts#L101) + +*** + +### MintTokenInput + +Defined in: [platform/integrations.ts:104](https://github.com/tangle-network/agent-runtime/blob/main/src/platform/integrations.ts#L104) + +#### Properties + +##### actionPath + +> **actionPath**: `string` + +Defined in: [platform/integrations.ts:106](https://github.com/tangle-network/agent-runtime/blob/main/src/platform/integrations.ts#L106) + +The hub action the token authorizes (e.g. `slack.chat.postMessage`). + +##### connectionId? + +> `optional` **connectionId?**: `string` + +Defined in: [platform/integrations.ts:108](https://github.com/tangle-network/agent-runtime/blob/main/src/platform/integrations.ts#L108) + +Bind to a specific connection, or … + +##### provider? + +> `optional` **provider?**: `string` + +Defined in: [platform/integrations.ts:110](https://github.com/tangle-network/agent-runtime/blob/main/src/platform/integrations.ts#L110) + +… resolve the connection by provider for the calling user. + +*** + +### MintTokenResult + +Defined in: [platform/integrations.ts:113](https://github.com/tangle-network/agent-runtime/blob/main/src/platform/integrations.ts#L113) + +#### Properties + +##### tokenId + +> **tokenId**: `string` + +Defined in: [platform/integrations.ts:114](https://github.com/tangle-network/agent-runtime/blob/main/src/platform/integrations.ts#L114) + +##### token + +> **token**: `string` + +Defined in: [platform/integrations.ts:115](https://github.com/tangle-network/agent-runtime/blob/main/src/platform/integrations.ts#L115) + +##### expiresAt + +> **expiresAt**: `string` + +Defined in: [platform/integrations.ts:116](https://github.com/tangle-network/agent-runtime/blob/main/src/platform/integrations.ts#L116) + +*** + +### ExecInput + +Defined in: [platform/integrations.ts:119](https://github.com/tangle-network/agent-runtime/blob/main/src/platform/integrations.ts#L119) + +#### Properties + +##### path + +> **path**: `string` + +Defined in: [platform/integrations.ts:121](https://github.com/tangle-network/agent-runtime/blob/main/src/platform/integrations.ts#L121) + +The hub action path to execute. + +##### input? + +> `optional` **input?**: `unknown` + +Defined in: [platform/integrations.ts:122](https://github.com/tangle-network/agent-runtime/blob/main/src/platform/integrations.ts#L122) + +##### connectionId? + +> `optional` **connectionId?**: `string` + +Defined in: [platform/integrations.ts:123](https://github.com/tangle-network/agent-runtime/blob/main/src/platform/integrations.ts#L123) + +*** + +### PlatformHubStatus + +Defined in: [platform/integrations.ts:126](https://github.com/tangle-network/agent-runtime/blob/main/src/platform/integrations.ts#L126) + +#### Properties + +##### contract? + +> `optional` **contract?**: `unknown` + +Defined in: [platform/integrations.ts:127](https://github.com/tangle-network/agent-runtime/blob/main/src/platform/integrations.ts#L127) + +##### principal + +> **principal**: `object` + +Defined in: [platform/integrations.ts:128](https://github.com/tangle-network/agent-runtime/blob/main/src/platform/integrations.ts#L128) + +###### Index Signature + +\[`k`: `string`\]: `unknown` + +###### kind + +> **kind**: `string` + +###### userId + +> **userId**: `string` + +##### connections + +> **connections**: `object` + +Defined in: [platform/integrations.ts:129](https://github.com/tangle-network/agent-runtime/blob/main/src/platform/integrations.ts#L129) + +###### connectedProviderCount + +> **connectedProviderCount**: `number` + +###### unhealthyProviderCount + +> **unhealthyProviderCount**: `number` diff --git a/docs/api/runtime.md b/docs/api/runtime.md index 6d44a097..662cc911 100644 --- a/docs/api/runtime.md +++ b/docs/api/runtime.md @@ -7108,7 +7108,7 @@ Defined in: [runtime/supervise/coordination-mcp.ts:46](https://github.com/tangle ### DelegateOptions -Defined in: [runtime/supervise/delegate.ts:37](https://github.com/tangle-network/agent-runtime/blob/main/src/runtime/supervise/delegate.ts#L37) +Defined in: [runtime/supervise/delegate.ts:38](https://github.com/tangle-network/agent-runtime/blob/main/src/runtime/supervise/delegate.ts#L38) Inputs to [delegate](#delegate). The intent is the first positional arg; everything here is optional with sensible defaults, so the common call is `delegate(intent, { backend, router })`. @@ -7125,7 +7125,7 @@ Inputs to [delegate](#delegate). The intent is the first positional arg; everyth > `readonly` `optional` **deliverable?**: [`DeliverableSpec`](#deliverablespec)\<`Out`\> -Defined in: [runtime/supervise/delegate.ts:41](https://github.com/tangle-network/agent-runtime/blob/main/src/runtime/supervise/delegate.ts#L41) +Defined in: [runtime/supervise/delegate.ts:42](https://github.com/tangle-network/agent-runtime/blob/main/src/runtime/supervise/delegate.ts#L42) The completion oracle (settled ⟺ delivered) the authored workers settle against. Strongly recommended — without it the supervisor trusts a worker's self-report. For a code intent, @@ -7135,7 +7135,7 @@ The completion oracle (settled ⟺ delivered) the authored workers settle agains > `readonly` `optional` **backend?**: [`ExecutorConfig`](#executorconfig) -Defined in: [runtime/supervise/delegate.ts:45](https://github.com/tangle-network/agent-runtime/blob/main/src/runtime/supervise/delegate.ts#L45) +Defined in: [runtime/supervise/delegate.ts:46](https://github.com/tangle-network/agent-runtime/blob/main/src/runtime/supervise/delegate.ts#L46) WHERE the authored workers run — the worker-execution backend (`router-tools` / `sandbox` / `cli-worktree` / …). The supervisor authors the worker PROFILE; this is the substrate it runs @@ -7145,7 +7145,7 @@ WHERE the authored workers run — the worker-execution backend (`router-tools` > `readonly` `optional` **budget?**: [`Budget`](#budget-10) -Defined in: [runtime/supervise/delegate.ts:47](https://github.com/tangle-network/agent-runtime/blob/main/src/runtime/supervise/delegate.ts#L47) +Defined in: [runtime/supervise/delegate.ts:48](https://github.com/tangle-network/agent-runtime/blob/main/src/runtime/supervise/delegate.ts#L48) The conserved compute pool for the whole delegation. Defaults to [defaultDelegateBudget](#defaultdelegatebudget). @@ -7153,7 +7153,7 @@ The conserved compute pool for the whole delegation. Defaults to [defaultDelegat > `readonly` `optional` **model?**: `string` -Defined in: [runtime/supervise/delegate.ts:50](https://github.com/tangle-network/agent-runtime/blob/main/src/runtime/supervise/delegate.ts#L50) +Defined in: [runtime/supervise/delegate.ts:51](https://github.com/tangle-network/agent-runtime/blob/main/src/runtime/supervise/delegate.ts#L51) The model the supervisor BRAIN runs on (the router model). The brain must tool-call (`spawn_agent` / `await_event`), so a delegator model, not a hidden-reasoning model. @@ -7162,7 +7162,7 @@ The model the supervisor BRAIN runs on (the router model). The brain must tool-c > `readonly` `optional` **router?**: [`RouterConfig`](#routerconfig) -Defined in: [runtime/supervise/delegate.ts:54](https://github.com/tangle-network/agent-runtime/blob/main/src/runtime/supervise/delegate.ts#L54) +Defined in: [runtime/supervise/delegate.ts:55](https://github.com/tangle-network/agent-runtime/blob/main/src/runtime/supervise/delegate.ts#L55) The supervisor brain's router substrate. REQUIRED for the default router-brained supervisor (the brain is resolved from this), unless a test injects `brain` directly. `model` overrides @@ -7172,7 +7172,7 @@ The supervisor brain's router substrate. REQUIRED for the default router-brained > `readonly` `optional` **brain?**: [`ToolLoopChat`](#toolloopchat) -Defined in: [runtime/supervise/delegate.ts:56](https://github.com/tangle-network/agent-runtime/blob/main/src/runtime/supervise/delegate.ts#L56) +Defined in: [runtime/supervise/delegate.ts:57](https://github.com/tangle-network/agent-runtime/blob/main/src/runtime/supervise/delegate.ts#L57) Inject the supervisor brain directly (tests / advanced) instead of resolving it from `router`. @@ -7180,7 +7180,7 @@ Inject the supervisor brain directly (tests / advanced) instead of resolving it > `readonly` `optional` **supervisor?**: `Partial`\<`Pick`\<[`SupervisorProfile`](#supervisorprofile), `"name"` \| `"systemPrompt"`\>\> -Defined in: [runtime/supervise/delegate.ts:59](https://github.com/tangle-network/agent-runtime/blob/main/src/runtime/supervise/delegate.ts#L59) +Defined in: [runtime/supervise/delegate.ts:60](https://github.com/tangle-network/agent-runtime/blob/main/src/runtime/supervise/delegate.ts#L60) Override the default authoring-supervisor profile (name / extra system-prompt stance). The default already carries the authoring skill; override only to add a goal or rename. @@ -7189,7 +7189,7 @@ Override the default authoring-supervisor profile (name / extra system-prompt st > `readonly` `optional` **allowedModels?**: readonly `string`[] -Defined in: [runtime/supervise/delegate.ts:61](https://github.com/tangle-network/agent-runtime/blob/main/src/runtime/supervise/delegate.ts#L61) +Defined in: [runtime/supervise/delegate.ts:62](https://github.com/tangle-network/agent-runtime/blob/main/src/runtime/supervise/delegate.ts#L62) Restrict the run to this subset of models (forwarded to `supervise()`). @@ -7197,7 +7197,7 @@ Restrict the run to this subset of models (forwarded to `supervise()`). > `readonly` `optional` **runId?**: `string` -Defined in: [runtime/supervise/delegate.ts:62](https://github.com/tangle-network/agent-runtime/blob/main/src/runtime/supervise/delegate.ts#L62) +Defined in: [runtime/supervise/delegate.ts:63](https://github.com/tangle-network/agent-runtime/blob/main/src/runtime/supervise/delegate.ts#L63) *** @@ -8878,7 +8878,7 @@ Lifecycle stream sink, threaded into the root `Scope` so every `spawn`/settle em ### WidenGate -Defined in: [runtime/supervise/types.ts:507](https://github.com/tangle-network/agent-runtime/blob/main/src/runtime/supervise/types.ts#L507) +Defined in: [runtime/supervise/types.ts:511](https://github.com/tangle-network/agent-runtime/blob/main/src/runtime/supervise/types.ts#L511) The progressive-widening gate (MCTS-PW). Decides whether a settled child is `promising` enough to spawn another under the remaining pool. DEFAULTS TO FLAT @@ -8899,7 +8899,7 @@ an explicit, argued `judgeExempt: true` (the documented escape hatch, off by def > `readonly` `optional` **judgeExempt?**: `boolean` -Defined in: [runtime/supervise/types.ts:512](https://github.com/tangle-network/agent-runtime/blob/main/src/runtime/supervise/types.ts#L512) +Defined in: [runtime/supervise/types.ts:516](https://github.com/tangle-network/agent-runtime/blob/main/src/runtime/supervise/types.ts#L516) When true, widening may read `verdict` directly (collides with the steer firewall — must be explicitly argued per cell, never defaulted on). @@ -8910,7 +8910,7 @@ When true, widening may read `verdict` directly (collides with the steer firewal > **shouldWiden**(`settled`, `budget`): `boolean` -Defined in: [runtime/supervise/types.ts:509](https://github.com/tangle-network/agent-runtime/blob/main/src/runtime/supervise/types.ts#L509) +Defined in: [runtime/supervise/types.ts:513](https://github.com/tangle-network/agent-runtime/blob/main/src/runtime/supervise/types.ts#L513) Default impl returns false for every settlement (flat — never widens). @@ -11978,7 +11978,7 @@ True = infrastructure failure (excluded from merge `n` / equal-k), not a bad res ### SupervisedResult -> **SupervisedResult**\<`Out`\> = \{ `kind`: `"winner"`; `out`: `Out`; `outRef`: `string`; `verdict?`: `DefaultVerdict`; `tree`: [`TreeView`](#treeview); `spentTotal`: [`Spend`](#spend); `spentBreakdown?`: \{ `driverInference`: [`Spend`](#spend); `childWork`: [`Spend`](#spend); \}; \} \| \{ `kind`: `"no-winner"`; `reason`: `"all-children-down"` \| `"budget-exhausted"` \| `"aborted"`; `tree`: [`TreeView`](#treeview); `downCount`: `number`; \} +> **SupervisedResult**\<`Out`\> = \{ `kind`: `"winner"`; `out`: `Out`; `outRef`: `string`; `verdict?`: `DefaultVerdict`; `tree`: [`TreeView`](#treeview); `spentTotal`: [`Spend`](#spend); `spentBreakdown?`: \{ `driverInference`: [`Spend`](#spend); `childWork`: [`Spend`](#spend); \}; \} \| \{ `kind`: `"no-winner"`; `reason`: `"all-children-down"` \| `"budget-exhausted"` \| `"aborted"`; `tree`: [`TreeView`](#treeview); `downCount`: `number`; `spentTotal`: [`Spend`](#spend); \} Defined in: [runtime/supervise/types.ts:459](https://github.com/tangle-network/agent-runtime/blob/main/src/runtime/supervise/types.ts#L459) @@ -12040,7 +12040,31 @@ Where `spentTotal` went: `driverInference` = the drivers' own chat turns (metere ##### Type Literal -\{ `kind`: `"no-winner"`; `reason`: `"all-children-down"` \| `"budget-exhausted"` \| `"aborted"`; `tree`: [`TreeView`](#treeview); `downCount`: `number`; \} +\{ `kind`: `"no-winner"`; `reason`: `"all-children-down"` \| `"budget-exhausted"` \| `"aborted"`; `tree`: [`TreeView`](#treeview); `downCount`: `number`; `spentTotal`: [`Spend`](#spend); \} + +###### kind + +> **kind**: `"no-winner"` + +###### reason + +> **reason**: `"all-children-down"` \| `"budget-exhausted"` \| `"aborted"` + +###### tree + +> **tree**: [`TreeView`](#treeview) + +###### downCount + +> **downCount**: `number` + +###### spentTotal + +> **spentTotal**: [`Spend`](#spend) + +The conserved spend incurred before the run failed — real cost is paid even when no + worker delivers, so the caller always learns what the delegation actually spent. Summed + off the same journal the `winner` path reads. *** @@ -12223,7 +12247,7 @@ Defined in: [runtime/supervise/authoring.ts:138](https://github.com/tangle-netwo > `const` **defaultDelegateBudget**: [`Budget`](#budget-10) -Defined in: [runtime/supervise/delegate.ts:33](https://github.com/tangle-network/agent-runtime/blob/main/src/runtime/supervise/delegate.ts#L33) +Defined in: [runtime/supervise/delegate.ts:34](https://github.com/tangle-network/agent-runtime/blob/main/src/runtime/supervise/delegate.ts#L34) The conserved pool a `delegate()` call applies when the caller does not pass its own `budget`. A modest token ceiling + a small iteration ceiling — generous enough for a few-worker decompose, @@ -14403,7 +14427,7 @@ Pass-through subscriber for every bus event (settled / question / finding). > **delegate**\<`Out`\>(`intent`, `opts?`): `Promise`\<[`SupervisedResult`](#supervisedresult)\<`Out`\>\> -Defined in: [runtime/supervise/delegate.ts:87](https://github.com/tangle-network/agent-runtime/blob/main/src/runtime/supervise/delegate.ts#L87) +Defined in: [runtime/supervise/delegate.ts:88](https://github.com/tangle-network/agent-runtime/blob/main/src/runtime/supervise/delegate.ts#L88) Delegate an INTENT to a default authoring supervisor and return its `SupervisedResult` unchanged. diff --git a/docs/canonical-api.md b/docs/canonical-api.md index cc8e6412..681535a2 100644 --- a/docs/canonical-api.md +++ b/docs/canonical-api.md @@ -2,7 +2,7 @@ -> **Version 0.70.0.** Per-symbol signatures live in the generated `docs/api/` reference (one page per module). The pinned substrate is agent-eval `>=0.95.0 <1.0.0`; the sandbox substrate that materializes profiles into harness shapes is `@tangle-network/sandbox` (peer `>=0.8.0 <1.0.0`). The neutral contract types (`AgentProfile`, `AgentProfileMcpServer`, `HarnessType`, `ReasoningEffort`, `Part`/`ToolPart`/`ToolState`) are owned by **`@tangle-network/agent-interface`** (peer `>=0.10.0 <1.0.0`) — the single source of truth. Substrate symbols (`selfImprove`/`gepaProposer`/`defaultProductionGate`/`heldOutGate`/`pairedBootstrap`/…) are re-exported through `@tangle-network/agent-eval/contract` (or `/campaign`), not local to this package. +> **Version 0.70.1.** Per-symbol signatures live in the generated `docs/api/` reference (one page per module). The pinned substrate is agent-eval `>=0.95.0 <1.0.0`; the sandbox substrate that materializes profiles into harness shapes is `@tangle-network/sandbox` (peer `>=0.8.0 <1.0.0`). The neutral contract types (`AgentProfile`, `AgentProfileMcpServer`, `HarnessType`, `ReasoningEffort`, `Part`/`ToolPart`/`ToolState`) are owned by **`@tangle-network/agent-interface`** (peer `>=0.10.0 <1.0.0`) — the single source of truth. Substrate symbols (`selfImprove`/`gepaProposer`/`defaultProductionGate`/`heldOutGate`/`pairedBootstrap`/…) are re-exported through `@tangle-network/agent-eval/contract` (or `/campaign`), not local to this package. > > **`./loops` is the runtime barrel** — `package.json` maps it to `src/runtime/index.ts`. Everything below labelled `/loops` is the recursive-atom + loop-kernel surface. > diff --git a/examples/delegate/e2e-delegate-real.ts b/examples/delegate/e2e-delegate-real.ts new file mode 100644 index 00000000..2608352c --- /dev/null +++ b/examples/delegate/e2e-delegate-real.ts @@ -0,0 +1,128 @@ +/** + * REAL e2e proof that `delegate(intent)` replaces a hardcoded coder: a router-brained supervisor + * AUTHORS + spawns a worker (no baked-in coder profile), the worker does real filesystem work via a + * granted `write_file` tool, and the deliverable gate settles ONLY when the file actually exists on + * disk. `result.spentTotal` carries the real conserved cost (tokens + usd) — the channel delegate_code + * lacks. Proves on BOTH paths: a winner carries the worker's spend; a no-winner carries the spend + * incurred before it failed. + * + * Run: TANGLE_API_KEY= pnpm tsx examples/delegate/e2e-delegate-real.ts + */ +import { existsSync, mkdirSync, mkdtempSync, readFileSync, writeFileSync } from 'node:fs' +import { tmpdir } from 'node:os' +import { dirname, join, resolve } from 'node:path' +import { delegate, type ExecutorConfig } from '../../dist/loops.js' + +const routerBaseUrl = process.env.TANGLE_ROUTER_URL ?? 'https://router.tangle.tools/v1' +const routerKey = process.env.TANGLE_API_KEY +if (!routerKey) throw new Error('set TANGLE_API_KEY (your Tangle router key)') +// Two distinct roles: the WORKER does the filesystem task (cheap model is fine); the supervisor +// BRAIN must reliably tool-call `spawn_agent` / `await_event`, so it defaults to a stronger +// delegator model. Override either with WORKER_MODEL / BRAIN_MODEL (or MODEL for both). +const model = process.env.MODEL ?? process.env.WORKER_MODEL ?? 'deepseek-v4-flash' +const brainModel = process.env.MODEL ?? process.env.BRAIN_MODEL ?? model + +// An isolated scratch dir so the deliverable check reads ground truth, not the model's claim. +const workDir = mkdtempSync(join(tmpdir(), 'delegate-e2e-')) +const target = 'out.txt' +const targetAbs = join(workDir, target) + +// The WORKER's granted tool: a real filesystem write, sandboxed to workDir. The supervisor authors +// the worker profile and decides to call this; the implementation lives here (the backend host). +const tools = [ + { + type: 'function' as const, + function: { + name: 'write_file', + description: 'Write text content to a file path (relative to the working directory).', + parameters: { + type: 'object', + properties: { + path: { type: 'string', description: 'Relative file path, e.g. out.txt' }, + content: { type: 'string', description: 'The exact text to write' }, + }, + required: ['path', 'content'], + additionalProperties: false, + }, + }, + }, +] + +async function writeFileTool(args: Record): Promise { + const rel = String(args.path ?? '') + const content = String(args.content ?? '') + // Confine to workDir — never escape the scratch dir. + const abs = resolve(workDir, rel) + if (!abs.startsWith(resolve(workDir))) return `error: path escapes the working directory` + mkdirSync(dirname(abs), { recursive: true }) + writeFileSync(abs, content, 'utf8') + return `wrote ${content.length} bytes to ${rel}` +} + +const backend: ExecutorConfig = { + backend: 'router-tools', + routerBaseUrl, + routerKey, + model, + tools, + executeToolCall: async (name, args) => { + if (name === 'write_file') return writeFileTool(args) + return `unknown tool ${name}` + }, + maxTurns: 8, +} + +const result = await delegate( + `Create a file named ${target} containing exactly the word hello (lowercase, no quotes). ` + + `Call the write_file tool exactly once with path="${target}" and content="hello", then reply ` + + `with the single word DONE and STOP — do not call any more tools after the file is written.`, + { + backend, + router: { routerBaseUrl, routerKey, model: brainModel }, + model: brainModel, + // The deployable completion oracle: settle ONLY when the file actually exists with the right + // content. This reads disk — ground truth — never the worker judging itself. + deliverable: { + check: () => existsSync(targetAbs) && readFileSync(targetAbs, 'utf8').trim() === 'hello', + describe: `file ${target} exists and contains "hello"`, + }, + // A real budget — enough for the supervisor to author+spawn a worker and the worker to finish. + budget: { maxIterations: 40, maxTokens: 200_000, maxUsd: 0.5 }, + }, +) + +const fileExists = existsSync(targetAbs) +const fileContent = fileExists ? readFileSync(targetAbs, 'utf8') : '' + +console.log('=== delegate() REAL e2e ===') +console.log('brain / worker :', brainModel, '/', model) +console.log('result.kind :', result.kind) +console.log('file exists :', fileExists, '@', targetAbs) +console.log('file content :', JSON.stringify(fileContent)) +console.log('spentTotal :', JSON.stringify(result.spentTotal)) +if (result.kind === 'winner') { + console.log('winner.outRef :', result.outRef) + console.log('worker out (text):', JSON.stringify(String(result.out).slice(0, 200))) +} +if (result.kind === 'no-winner') { + console.log('no-winner reason:', result.reason, '| downCount:', result.downCount) + // Surface each child node's terminal status + its own spend so a transient infra flake is + // distinguishable from a real failure to deliver. + for (const n of result.tree.nodes) { + console.log(' node:', JSON.stringify({ id: n.id, status: n.status, spent: n.spent })) + } +} + +// The proof gates: file met by the WORKER, real cost rode through, supervisor authored (not hardcoded). +const tokensReal = result.spentTotal.tokens.input + result.spentTotal.tokens.output > 0 +const costRodeThrough = result.spentTotal.usd > 0 || tokensReal +const delivered = result.kind === 'winner' && fileExists && fileContent.trim() === 'hello' + +console.log('--- gates ---') +console.log('PROOF worker completed the task (file delivered):', delivered) +console.log('PROOF cost rode through (real tokens/usd) :', costRodeThrough) +if (!delivered || !costRodeThrough) { + console.error('E2E FAILED: deliverable not met or cost did not ride through') + process.exit(1) +} +console.log('E2E PASSED') diff --git a/src/mcp/tools/delegate.ts b/src/mcp/tools/delegate.ts index 837c9ae2..356579bb 100644 --- a/src/mcp/tools/delegate.ts +++ b/src/mcp/tools/delegate.ts @@ -116,13 +116,12 @@ export interface DelegateHandlerOptions { allowedModels?: readonly string[] } -const zeroSpend: Spend = { iterations: 0, tokens: { input: 0, output: 0 }, usd: 0, ms: 0 } - -/** Project a `SupervisedResult` onto the tool's flat `DelegateResult`. A no-winner carries no spend - * on the result union, so it reports zero spend with the failure reason — never a faked output. */ +/** Project a `SupervisedResult` onto the tool's flat `DelegateResult`. Both variants carry the real + * conserved `spentTotal`, so the agent always learns the cost — even on a no-winner, never a faked + * output and never a fabricated zero spend. */ function toDelegateResult(result: SupervisedResult): DelegateResult { if (result.kind === 'no-winner') { - return { status: 'no-winner', reason: result.reason, spentTotal: zeroSpend } + return { status: 'no-winner', reason: result.reason, spentTotal: result.spentTotal } } return { status: 'winner', diff --git a/src/runtime/supervise/delegate.ts b/src/runtime/supervise/delegate.ts index a712c81c..d9769f10 100644 --- a/src/runtime/supervise/delegate.ts +++ b/src/runtime/supervise/delegate.ts @@ -13,8 +13,9 @@ * completion oracle (`deliverable`), the coordination toolbox, and equal-compute accounting all come * for free; nothing is hand-rolled. The result is `supervise()`'s `SupervisedResult` returned * UNCHANGED, so its `spentTotal` (`{ iterations, tokens, usd, ms }`) rides straight back to the - * caller. That cost channel is exactly what `delegate_code` lacks — a `delegate()` caller always - * learns what the delegation actually spent. + * caller on BOTH paths — a `winner` carries the delivered worker's spend, a `no-winner` carries the + * spend incurred before it failed. That cost channel is exactly what `delegate_code` lacks — a + * `delegate()` caller always learns what the delegation actually spent. */ import { ConfigError } from '../../errors' diff --git a/src/runtime/supervise/supervisor.ts b/src/runtime/supervise/supervisor.ts index 2c7d06b5..a58facba 100644 --- a/src/runtime/supervise/supervisor.ts +++ b/src/runtime/supervise/supervisor.ts @@ -189,23 +189,26 @@ export function createSupervisor(): Supervisor { : {}), } } - return { - kind: 'no-winner', - reason: classifyNoWinner(controller, pool, opts, breaker), - tree, - downCount: breaker.downCount(), - } + return noWinner() } // act() rejected. The reason is proven from lifecycle state, in precedence order: // a tripped breaker outranks any abort (it is the most specific cause) outranks // budget-exhaustion outranks the residual "the tree produced nothing usable" bucket. // A no-winner is TYPED — never a best-effort coercion of a partial child (M2). - return { - kind: 'no-winner', - reason: classifyNoWinner(controller, pool, opts, breaker), - tree, - downCount: breaker.downCount(), + return noWinner() + + // A no-winner still incurred real conserved spend before failing, so it carries `spentTotal` + // summed off the SAME journal the winner path reads — the caller always learns the cost. + async function noWinner(): Promise> { + const { childWork, driverInference } = await spentFromJournal(journal, opts.runId) + return { + kind: 'no-winner', + reason: classifyNoWinner(controller, pool, opts, breaker), + tree, + downCount: breaker.downCount(), + spentTotal: addSpend(childWork, driverInference), + } } } diff --git a/src/runtime/supervise/types.ts b/src/runtime/supervise/types.ts index edee432c..8b45a98f 100644 --- a/src/runtime/supervise/types.ts +++ b/src/runtime/supervise/types.ts @@ -474,6 +474,10 @@ export type SupervisedResult = reason: 'all-children-down' | 'budget-exhausted' | 'aborted' tree: TreeView downCount: number + /** The conserved spend incurred before the run failed — real cost is paid even when no + * worker delivers, so the caller always learns what the delegation actually spent. Summed + * off the same journal the `winner` path reads. */ + spentTotal: Spend } /** Live root handle — the substrate a chat/pi-viz client attaches to (Q2). `signal` diff --git a/tests/loops/delegate.test.ts b/tests/loops/delegate.test.ts index 65819203..d1d16207 100644 --- a/tests/loops/delegate.test.ts +++ b/tests/loops/delegate.test.ts @@ -94,17 +94,22 @@ describe('delegate — the one generic delegation verb over supervise()', () => } }) - it('forwards a no-winner result faithfully (never fabricates a success)', async () => { + it('forwards a no-winner result faithfully — including its real spend (never fabricates a success or zero cost)', async () => { const noWinner: SupervisedResult = { kind: 'no-winner', reason: 'budget-exhausted', tree: emptyTree, downCount: 1, + spentTotal, } superviseSpy.mockResolvedValue(noWinner) const result = await delegate('do the thing', { backend, router }) expect(result.kind).toBe('no-winner') + if (result.kind === 'no-winner') { + // A budget-exhausted delegation still cost real compute; the spend rides back unchanged. + expect(result.spentTotal).toEqual(spentTotal) + } }) it('forwards deliverable, model, budget, allowedModels, runId to supervise()', async () => { diff --git a/tests/loops/supervise.test.ts b/tests/loops/supervise.test.ts index 2a4e7d4d..acc3415b 100644 --- a/tests/loops/supervise.test.ts +++ b/tests/loops/supervise.test.ts @@ -710,6 +710,13 @@ describe('supervisor', () => { if (result.kind === 'no-winner') { expect(result.reason).toBe('all-children-down') expect(result.downCount).toBe(2) + // A no-winner still carries the conserved spend, summed off the same journal the winner path + // reads — well-formed (every channel present, non-negative), never absent or fabricated. + expect(result.spentTotal).toBeDefined() + expect(result.spentTotal.iterations).toBeGreaterThanOrEqual(0) + expect(result.spentTotal.tokens.input).toBeGreaterThanOrEqual(0) + expect(result.spentTotal.tokens.output).toBeGreaterThanOrEqual(0) + expect(result.spentTotal.usd).toBeGreaterThanOrEqual(0) } }) diff --git a/tests/mcp/delegate.test.ts b/tests/mcp/delegate.test.ts index eecadf91..d10a1489 100644 --- a/tests/mcp/delegate.test.ts +++ b/tests/mcp/delegate.test.ts @@ -86,17 +86,24 @@ describe('delegate MCP tool — generic delegation verb that returns cost', () = expect(result.spentTotal).toEqual(spentTotal) }) - it('reports no-winner faithfully with the reason (never a faked success)', async () => { + it('reports no-winner faithfully with the reason AND the real spend (never a faked success or zero cost)', async () => { superviseSpy.mockResolvedValue({ kind: 'no-winner', reason: 'all-children-down', tree: emptyTree, downCount: 2, + spentTotal, }) const handler = createDelegateHandler({ router, backend }) - const result = (await handler({ intent: 'do x' })) as { status: string; reason: string } + const result = (await handler({ intent: 'do x' })) as { + status: string + reason: string + spentTotal: Spend + } expect(result.status).toBe('no-winner') expect(result.reason).toBe('all-children-down') + // A failed delegation still cost real compute; the agent must learn it — never a fabricated zero. + expect(result.spentTotal).toEqual(spentTotal) }) it('applies a per-call model override', async () => { diff --git a/typedoc.json b/typedoc.json index d88e6c7a..fda83035 100644 --- a/typedoc.json +++ b/typedoc.json @@ -7,6 +7,7 @@ "src/runtime/index.ts", "src/analyst-loop/index.ts", "src/profiles/index.ts", + "src/platform/index.ts", "src/mcp/index.ts" ], "entryPointStrategy": "resolve",