eve 0.74.0 → 0.75.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (186) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/dist/src/channel/auth/result.d.ts +4 -0
  3. package/dist/src/channel/auth/result.js +1 -0
  4. package/dist/src/channel/types.d.ts +14 -2
  5. package/dist/src/chunks/{use-eve-agent-mP0M7SrL.js → use-eve-agent-B2Kne9z6.js} +7 -2
  6. package/dist/src/chunks/{use-eve-agent-DOOXadF0.js → use-eve-agent-DK_T88EP.js} +7 -2
  7. package/dist/src/cli/commands/integration-connect.d.ts +2 -0
  8. package/dist/src/cli/commands/integration-connect.js +1 -1
  9. package/dist/src/cli/commands/integration-setup.js +1 -1
  10. package/dist/src/cli/commands/registry-declared-setups.d.ts +19 -0
  11. package/dist/src/cli/commands/registry-declared-setups.js +1 -1
  12. package/dist/src/cli/commands/registry-recovery.d.ts +3 -1
  13. package/dist/src/cli/commands/registry-recovery.js +1 -1
  14. package/dist/src/cli/commands/registry.d.ts +12 -4
  15. package/dist/src/cli/commands/registry.js +1 -1
  16. package/dist/src/cli/dev/tui/registry-result-message.js +1 -1
  17. package/dist/src/cli/dev/tui/setup-commands.d.ts +2 -0
  18. package/dist/src/cli/dev/tui/setup-commands.js +2 -2
  19. package/dist/src/client/index.d.ts +1 -0
  20. package/dist/src/client/session.d.ts +1 -1
  21. package/dist/src/client/session.js +1 -1
  22. package/dist/src/client/sessions.d.ts +1 -1
  23. package/dist/src/client/types.d.ts +6 -0
  24. package/dist/src/compiled/.vendor-stamp.json +1 -1
  25. package/dist/src/compiler/extension-compatibility.d.ts +18 -12
  26. package/dist/src/compiler/extension-compatibility.js +1 -1
  27. package/dist/src/context/dynamic-connection-lifecycle.d.ts +6 -0
  28. package/dist/src/context/dynamic-connection-lifecycle.js +1 -1
  29. package/dist/src/context/keys.d.ts +4 -1
  30. package/dist/src/context/keys.js +1 -1
  31. package/dist/src/context/providers/sandbox.js +1 -1
  32. package/dist/src/context/providers/tool-stubs-key.d.ts +8 -0
  33. package/dist/src/context/providers/tool-stubs-key.js +1 -0
  34. package/dist/src/context/providers/tool-stubs.d.ts +10 -0
  35. package/dist/src/context/providers/tool-stubs.js +1 -0
  36. package/dist/src/context/run-step.js +1 -1
  37. package/dist/src/evals/index.d.ts +1 -0
  38. package/dist/src/evals/runner/execute-task.js +1 -1
  39. package/dist/src/evals/session-manager.d.ts +2 -0
  40. package/dist/src/evals/session-manager.js +1 -1
  41. package/dist/src/eve-channel/create-request.d.ts +2 -0
  42. package/dist/src/eve-channel/index.js +1 -1
  43. package/dist/src/eve-channel/request.js +1 -1
  44. package/dist/src/execution/agent-sessions/context.d.ts +2 -0
  45. package/dist/src/execution/agent-sessions/context.js +1 -1
  46. package/dist/src/execution/agent-sessions/steps.js +1 -1
  47. package/dist/src/execution/cancelled-model-call-batch.js +1 -1
  48. package/dist/src/execution/dynamic-connection-recovery.d.ts +14 -0
  49. package/dist/src/execution/dynamic-connection-recovery.js +1 -0
  50. package/dist/src/execution/dynamic-workflow/tool.js +1 -1
  51. package/dist/src/execution/forward-session-input.js +1 -1
  52. package/dist/src/execution/legacy-session/authorization.js +1 -1
  53. package/dist/src/execution/legacy-session/completion-step.js +1 -1
  54. package/dist/src/execution/legacy-session/inbox.js +1 -1
  55. package/dist/src/execution/legacy-session/interrupt-step.js +1 -1
  56. package/dist/src/execution/proxied-deliver-step.js +1 -1
  57. package/dist/src/execution/runtime-context.js +1 -1
  58. package/dist/src/execution/sandbox/bindings/docker.js +1 -1
  59. package/dist/src/execution/sandbox/bindings/just-bash-runtime.js +1 -1
  60. package/dist/src/execution/sandbox/bindings/just-bash.js +1 -1
  61. package/dist/src/execution/sandbox/bindings/microsandbox-lifecycle.js +1 -1
  62. package/dist/src/execution/sandbox/bindings/vercel.js +1 -1
  63. package/dist/src/execution/sandbox/ensure.d.ts +3 -1
  64. package/dist/src/execution/sandbox/ensure.js +1 -1
  65. package/dist/src/execution/session/active-turn.js +1 -1
  66. package/dist/src/execution/session/admission.js +1 -1
  67. package/dist/src/execution/session/checkpoint-migrations.js +1 -1
  68. package/dist/src/execution/session/end-sandbox-step.d.ts +12 -0
  69. package/dist/src/execution/session/end-sandbox-step.js +1 -0
  70. package/dist/src/execution/session/finalization.js +1 -1
  71. package/dist/src/execution/session/handoff-steps.js +1 -1
  72. package/dist/src/execution/session/handoff.d.ts +1 -1
  73. package/dist/src/execution/session/handoff.js +1 -1
  74. package/dist/src/execution/session/program.js +1 -1
  75. package/dist/src/execution/session/timeout-steps.js +1 -1
  76. package/dist/src/execution/session/turn-step.js +1 -1
  77. package/dist/src/execution/session-inbox/inbox.d.ts +6 -1
  78. package/dist/src/execution/session-inbox/inbox.js +1 -1
  79. package/dist/src/execution/session-inbox/release-step.js +1 -1
  80. package/dist/src/execution/session-inbox/resume.d.ts +2 -1
  81. package/dist/src/execution/session-inbox/resume.js +1 -1
  82. package/dist/src/execution/session-workflow-tool-run.js +1 -1
  83. package/dist/src/execution/settle-cancelled-turn-step.js +1 -1
  84. package/dist/src/execution/tasks/model-step.js +1 -1
  85. package/dist/src/execution/tasks/steps.js +1 -1
  86. package/dist/src/execution/tasks/table.d.ts +7 -1
  87. package/dist/src/execution/tasks/table.js +1 -1
  88. package/dist/src/execution/tool-stubs/playback.d.ts +11 -0
  89. package/dist/src/execution/tool-stubs/playback.js +1 -0
  90. package/dist/src/execution/tool-stubs/steps.d.ts +18 -0
  91. package/dist/src/execution/tool-stubs/steps.js +1 -0
  92. package/dist/src/execution/tools/agent-router-workflow.js +1 -1
  93. package/dist/src/execution/tools/connection-tools.d.ts +0 -6
  94. package/dist/src/execution/tools/connection-tools.js +2 -2
  95. package/dist/src/execution/tools/workflow/authorization-completion.js +1 -1
  96. package/dist/src/execution/tools/workflow/body.js +1 -1
  97. package/dist/src/execution/tools/workflow/serve.js +1 -1
  98. package/dist/src/execution/tools/workflow/withdraw-step.js +1 -1
  99. package/dist/src/execution/workflow-runtime.js +1 -1
  100. package/dist/src/execution/workflow-webhook-route.js +1 -1
  101. package/dist/src/harness/coordination.js +1 -1
  102. package/dist/src/harness/hitl/approved-calls.d.ts +2 -0
  103. package/dist/src/harness/hitl/approved-calls.js +1 -1
  104. package/dist/src/harness/hitl/index.d.ts +9 -1
  105. package/dist/src/harness/hitl/index.js +1 -1
  106. package/dist/src/harness/hitl/intake.js +1 -1
  107. package/dist/src/harness/model-call/run.js +1 -1
  108. package/dist/src/harness/proxy-input-requests.d.ts +17 -6
  109. package/dist/src/harness/proxy-input-requests.js +1 -1
  110. package/dist/src/harness/session-machine/current.d.ts +4 -0
  111. package/dist/src/harness/session-machine/current.js +1 -1
  112. package/dist/src/harness/step/after-model.js +1 -1
  113. package/dist/src/harness/step/context.d.ts +7 -2
  114. package/dist/src/harness/step/context.js +1 -1
  115. package/dist/src/harness/tool-call-io.js +1 -1
  116. package/dist/src/harness/tool-loop.js +1 -1
  117. package/dist/src/harness/tools.js +1 -1
  118. package/dist/src/internal/application/package.js +1 -1
  119. package/dist/src/internal/invocation/workflow-execution.js +1 -1
  120. package/dist/src/internal/nitro/routes/channel-dispatch.js +1 -1
  121. package/dist/src/internal/workflow/cancel-expired-development-run.js +1 -1
  122. package/dist/src/protocol/routes.d.ts +4 -0
  123. package/dist/src/protocol/routes.js +1 -1
  124. package/dist/src/public/channels/auth.d.ts +21 -4
  125. package/dist/src/public/channels/auth.js +1 -1
  126. package/dist/src/public/channels/mcp.js +1 -1
  127. package/dist/src/public/schedules/providers/in-memory.js +1 -1
  128. package/dist/src/public/schedules/providers/vercel.js +1 -1
  129. package/dist/src/public/schedules/subscription.d.ts +0 -1
  130. package/dist/src/sandbox/state.d.ts +1 -0
  131. package/dist/src/self-modification/extension/subagents/agent/instructions.js +1 -1
  132. package/dist/src/self-modification/extension/subagents/agent/tools/registry_add.js +1 -1
  133. package/dist/src/setup/flows/registry-session.d.ts +10 -2
  134. package/dist/src/setup/flows/registry-session.js +1 -1
  135. package/dist/src/setup/flows/registry.d.ts +2 -0
  136. package/dist/src/setup/flows/registry.js +1 -1
  137. package/dist/src/setup/integrations/shared/prerequisite.d.ts +8 -0
  138. package/dist/src/setup/integrations/shared/prerequisite.js +1 -1
  139. package/dist/src/setup/registry-setup-client.js +1 -1
  140. package/dist/src/setup/scaffold/create/project.js +1 -1
  141. package/dist/src/setup/scaffold/index.js +1 -1
  142. package/dist/src/setup/vercel-project.d.ts +1 -2
  143. package/dist/src/setup/vercel-project.js +0 -0
  144. package/dist/src/shared/sandbox-provider.d.ts +9 -2
  145. package/dist/src/subagents/adapter.d.ts +3 -2
  146. package/dist/src/subagents/adapter.js +1 -1
  147. package/dist/src/subagents/callback-route.js +1 -1
  148. package/dist/src/subagents/hitl-proxy.d.ts +4 -3
  149. package/dist/src/subagents/hitl-proxy.js +1 -1
  150. package/dist/src/subagents/parent-notification.js +1 -1
  151. package/dist/src/svelte/index.js +1 -1
  152. package/dist/src/svelte/use-eve-agent.js +1 -1
  153. package/dist/src/tool-stubs/execute.d.ts +13 -0
  154. package/dist/src/tool-stubs/execute.js +1 -0
  155. package/dist/src/tool-stubs/output.d.ts +4 -0
  156. package/dist/src/tool-stubs/output.js +1 -0
  157. package/dist/src/tool-stubs/playback.d.ts +15 -0
  158. package/dist/src/tool-stubs/playback.js +1 -0
  159. package/dist/src/tool-stubs/rules.d.ts +3 -0
  160. package/dist/src/tool-stubs/rules.js +1 -0
  161. package/dist/src/tool-stubs/schema.d.ts +4 -0
  162. package/dist/src/tool-stubs/schema.js +1 -0
  163. package/dist/src/tool-stubs/target.d.ts +8 -0
  164. package/dist/src/tool-stubs/target.js +1 -0
  165. package/dist/src/tool-stubs/types.d.ts +56 -0
  166. package/dist/src/tool-stubs/types.js +1 -0
  167. package/dist/src/tool-stubs/validate-targets.d.ts +6 -0
  168. package/dist/src/tool-stubs/validate-targets.js +1 -0
  169. package/dist/src/tools/provided/ask-question-workflow.js +1 -1
  170. package/dist/src/tools/provided/sleep-workflow.js +1 -1
  171. package/dist/src/vue/index.js +1 -1
  172. package/dist/src/vue/use-eve-agent.js +1 -1
  173. package/docs/concepts/sessions-runs-and-streaming.md +2 -2
  174. package/docs/evals/meta.json +10 -1
  175. package/docs/evals/overview.mdx +2 -0
  176. package/docs/evals/tool-stubs.mdx +257 -0
  177. package/docs/extensions.md +1 -1
  178. package/docs/guides/auth-and-route-protection.md +25 -0
  179. package/docs/guides/dynamic-capabilities.md +10 -3
  180. package/docs/guides/frontend/nextjs.mdx +17 -4
  181. package/docs/sandbox/index.mdx +10 -2
  182. package/docs/subagents/index.mdx +1 -1
  183. package/docs/tools/human-in-the-loop.md +1 -1
  184. package/docs/tools/overview.mdx +8 -4
  185. package/docs/tools/workflows.mdx +15 -7
  186. package/package.json +1 -1
@@ -1,4 +1,4 @@
1
- import { a as conversationProjection, i as openConversationInputs, n as conversationReducer, o as callStatus, r as defaultMessageReducer, t as useEveAgent } from "../chunks/use-eve-agent-mP0M7SrL.js";
1
+ import { a as conversationProjection, i as openConversationInputs, n as conversationReducer, o as callStatus, r as defaultMessageReducer, t as useEveAgent } from "../chunks/use-eve-agent-B2Kne9z6.js";
2
2
 
3
3
  //#region src/client/tool-call-state.ts
4
4
  function toolCallState(conversation, callId, options = {}) {
@@ -1,3 +1,3 @@
1
- import { t as useEveAgent } from "../chunks/use-eve-agent-mP0M7SrL.js";
1
+ import { t as useEveAgent } from "../chunks/use-eve-agent-B2Kne9z6.js";
2
2
 
3
3
  export { useEveAgent };
@@ -0,0 +1,13 @@
1
+ import type { StubResult } from "#tool-stubs/types.js";
2
+ import type { ToolExecuteOptions } from "#tools/definition.js";
3
+ /** Tool inputs and approvals have already been checked before a stub can replace execution. */
4
+ export declare function executeWithToolStub(tool: string, input: unknown, options: ToolExecuteOptions, execute: () => unknown): unknown;
5
+ /** The connection wrapper checks operation inputs and approvals before requesting a stub. */
6
+ export declare function connectionToolStub(tool: string, input: unknown, callId: string): Promise<StubResult>;
7
+ /** Record errors converting stub responses so eval verification can detect them. */
8
+ export declare function recordToolStubFailure(tool: string, callId: string | undefined, turnId?: string): Promise<void>;
9
+ /** Record stub failures even when the caller recovers with fallback output. */
10
+ export declare function observeToolOutput<T>(tool: string, calls: readonly {
11
+ readonly callId: string;
12
+ readonly turnId?: string;
13
+ }[], project: () => T | Promise<T>, recover?: (error: unknown) => T): Promise<T>;
@@ -0,0 +1 @@
1
+ import{contextStorage}from"#context/container.js";import{SessionKey,ToolStubsKey}from"#context/keys.js";import{ToolStubPlaybackKey}from"#context/providers/tool-stubs-key.js";import{findStubTarget,stubCallId}from"#tool-stubs/target.js";import{isAsyncIterable}from"#shared/async-iterable.js";import{toolStubOutput}from"#tool-stubs/output.js";function executeWithToolStub(e,t,n,r){let i=contextStubCall(e,t,n.toolCallId);return i===void 0?r():executeStubbedTool(i.playback,i.call,n,r)}async function*executeStubbedTool(e,t,n,r){n.abortSignal?.throwIfAborted();let i=await e.call(t);if(i.kind===`error`)throw Error(i.error);if(i.kind===`stub`){yield toolStubOutput(i.outcome);return}let a=await r();isAsyncIterable(a)?yield*a:yield a}async function connectionToolStub(e,t,n){let r=contextStubCall(e,t,n);return r===void 0?{kind:`real`}:await r.playback.call(r.call)}function contextStubCall(e,t,n){let r=contextStorage.getStore();if(r===void 0)return;let i=findStubTarget(r.get(ToolStubsKey),e);if(i===void 0)return;let a=r.require(SessionKey);return{playback:r.require(ToolStubPlaybackKey),call:{tool:i.tool,input:t,callId:stubCallId(a.sessionId,a.turn.id,n)}}}async function recordToolStubFailure(e,t,n){let r=contextStorage.getStore(),i=r?.get(ToolStubsKey);if(i===void 0||t===void 0||e!==`connection_execute`&&findStubTarget(i,e)===void 0)return;let a=r.require(SessionKey);await r.require(ToolStubPlaybackKey).fail(stubCallId(a.sessionId,n??a.turn.id,t),`Stubbed tool "${e}" failed during output processing.`)}async function observeToolOutput(e,t,n,r){try{return await n()}catch(n){try{for(let n of t)await recordToolStubFailure(e,n.callId,n.turnId)}catch(e){throw AggregateError([n,e],`Tool output processing and failure reporting failed.`,{cause:n})}if(r!==void 0)return r(n);throw n}}export{connectionToolStub,executeWithToolStub,observeToolOutput,recordToolStubFailure};
@@ -0,0 +1,4 @@
1
+ import type { JsonValue } from "#shared/json.js";
2
+ import type { ToolStubOutcome } from "#tool-stubs/types.js";
3
+ /** Throws at the tool boundary, after the outcome has been recorded for replay. */
4
+ export declare function toolStubOutput(outcome: ToolStubOutcome): JsonValue;
@@ -0,0 +1 @@
1
+ function toolStubOutput(e){if(e.throw!==void 0){let t=Error(e.throw.message);throw t.name=e.throw.name??`Error`,t}return e.response}export{toolStubOutput};
@@ -0,0 +1,15 @@
1
+ import type { StubCall, StubResult, ToolStub } from "#tool-stubs/types.js";
2
+ /**
3
+ * Selects stub outcomes and advances their sequences.
4
+ * The surrounding workflow orders calls and restores progress on replay.
5
+ */
6
+ export declare class StubPlayback {
7
+ private readonly rules;
8
+ private readonly results;
9
+ private readonly positions;
10
+ constructor(rules: readonly ToolStub[]);
11
+ get matchedRuleCount(): number;
12
+ call(call: StubCall): StubResult;
13
+ private record;
14
+ fail(callId: string, error: string): StubResult;
15
+ }
@@ -0,0 +1 @@
1
+ import{isObject}from"#shared/guards.js";import{createStubValidator}from"#tool-stubs/schema.js";var StubPlayback=class{rules;results=new Map;positions=new Map;constructor(e){this.rules=e.map(e=>({...e,constraints:Object.entries(e.match??{}).map(([t,n])=>{try{return{property:t,validator:createStubValidator(n)}}catch(n){throw Error(`Could not create validator for matcher "${t}" in tool stub "${e.id}".`,{cause:n})}})}))}get matchedRuleCount(){return this.positions.size}call(e){let t=this.results.get(e.callId);if(t!==void 0)return t;if(this.results.size>=1e4)return{kind:`error`,error:`Tool stub session exceeded 10,000 calls. Start a new eval session.`};let n=this.rules.filter(t=>t.tool===e.tool);if(e.persistent&&n.some(e=>e.constraints.length>0))return this.record(e,{kind:`error`,error:`Persistent tool "${e.tool}" requires an unconditional stub.`});let r=n.find(t=>t.constraints.every(({property:t,validator:n})=>isObject(e.input)&&Object.hasOwn(e.input,t)&&n.validate(e.input[t]).valid));if(r===void 0)return this.record(e,{kind:`real`});let i=r.outcomes??[r.outcome],a=Math.min(this.positions.get(r.id)??0,i.length-1);return this.positions.set(r.id,a+1),this.record(e,{kind:`stub`,ruleId:r.id,position:a,outcome:i[a]})}record(e,t){return this.results.set(e.callId,t),t}fail(e,t){return this.results.get(e)?.kind===`stub`?{kind:`error`,error:t}:{kind:`real`}}};export{StubPlayback};
@@ -0,0 +1,3 @@
1
+ import type { ToolStub } from "#tool-stubs/types.js";
2
+ /** Validates stubs before creating the session. */
3
+ export declare function parseToolStubs(value: unknown): readonly ToolStub[];
@@ -0,0 +1 @@
1
+ import{z}from"#compiled/zod/index.js";import{parseJsonValue}from"#shared/json.js";import{createStubValidator,validateStubConstraint}from"#tool-stubs/schema.js";const ruleSchema=z.strictObject({id:z.string().min(1),tool:z.string().min(1),match:z.record(z.string(),z.union([z.boolean(),z.record(z.string(),z.json())])).optional()}),outcomeSchema=z.union([z.strictObject({response:z.json()}),z.strictObject({throw:z.strictObject({message:z.string(),name:z.string().min(1).optional()})})]),toolStubsSchema=z.array(z.union([ruleSchema.extend({outcome:outcomeSchema}),ruleSchema.extend({outcomes:z.tuple([outcomeSchema]).rest(outcomeSchema)})])).max(100);function parseToolStubs(e){boundStubConfiguration(e);let t=toolStubsSchema.parse(parseJsonValue(e)),n=new Set;for(let e of t){if(n.has(e.id))throw Error(`Duplicate tool stub id "${e.id}".`);if(n.add(e.id),[`connection_execute`,`connection_search`].includes(e.tool.split(`/`).at(-1)))throw Error(`Cannot stub connection_execute or connection_search. Stub a specific connection operation, such as linear__list_issues.`);for(let[t,n]of Object.entries(e.match??{})){validateStubConstraint(n,e.id,t);try{createStubValidator(n)}catch(n){throw Error(`Could not create matcher "${t}" in tool stub "${e.id}".`,{cause:n})}}}return t}function boundStubConfiguration(e){let t=[{value:e,depth:0}],n=0,r=0;for(;t.length>0;){let e=t.pop();if(++n>2e4||e.depth>32)throw Error(`Tool stubs exceed the size or nesting limit.`);if(typeof e.value==`string`&&(r+=e.value.length),e.value!==null&&typeof e.value==`object`)for(let[n,i]of Object.entries(e.value)){if(n===`__proto__`)throw Error(`Tool stubs cannot contain prototype keys.`);r+=n.length,t.push({value:i,depth:e.depth+1})}if(r>1e6)throw Error(`Tool stubs exceed the size or nesting limit.`)}}export{parseToolStubs};
@@ -0,0 +1,4 @@
1
+ import { Validator } from "#compiled/@cfworker/json-schema/index.js";
2
+ import type { JsonObject } from "#shared/json.js";
3
+ export declare function validateStubConstraint(value: unknown, ruleId: string, property: string): void;
4
+ export declare function createStubValidator(value: JsonObject | boolean): Validator;
@@ -0,0 +1 @@
1
+ import{Validator}from"#compiled/@cfworker/json-schema/index.js";const schema={$ref:`#`},schemaList={type:`array`,minItems:1,items:schema},propertyName={type:`string`,not:{enum:Object.getOwnPropertyNames(Object.prototype)}},schemaMap={type:`object`,propertyNames:propertyName,additionalProperties:schema},strings={type:`array`,uniqueItems:!0,items:propertyName},nonnegativeInteger={type:`integer`,minimum:0},jsonType={enum:[`array`,`boolean`,`integer`,`null`,`number`,`object`,`string`]},constraintSchema=new Validator({anyOf:[{type:`boolean`},{type:`object`,additionalProperties:!1,properties:{type:{anyOf:[jsonType,{type:`array`,minItems:1,uniqueItems:!0,items:jsonType}]},const:{type:[`string`,`number`,`boolean`,`null`]},enum:{type:`array`,minItems:1,uniqueItems:!0,items:{type:[`string`,`number`,`boolean`,`null`]}},title:{type:`string`},description:{type:`string`},default:!0,minimum:{type:`number`},maximum:{type:`number`},exclusiveMinimum:{type:`number`},exclusiveMaximum:{type:`number`},minLength:nonnegativeInteger,maxLength:nonnegativeInteger,pattern:{type:`string`,format:`regex`},minItems:nonnegativeInteger,maxItems:nonnegativeInteger,items:schema,prefixItems:schemaList,contains:schema,minProperties:nonnegativeInteger,maxProperties:nonnegativeInteger,properties:schemaMap,propertyNames:schema,required:strings,additionalProperties:schema,allOf:schemaList,anyOf:schemaList,oneOf:schemaList,not:schema,if:schema,then:schema,else:schema}}]},`2020-12`);function validateStubConstraint(e,t,n){let r=constraintSchema.validate(e);if(!r.valid){let e=r.errors.at(-1),i=e.keyword===`false`?`Unsupported JSON Schema keyword.`:e.error;throw Error(`Invalid matcher "${n}" in tool stub "${t}" at ${e.instanceLocation}: ${i}`)}}function createStubValidator(e){return new Validator(structuredClone(e),`2020-12`)}export{createStubValidator,validateStubConstraint};
@@ -0,0 +1,8 @@
1
+ import type { StubScope } from "#tool-stubs/types.js";
2
+ /** Qualify names for stub matching without changing the names sent to the model. */
3
+ export declare function stubToolPath(scope: StubScope | undefined, tool: string): string;
4
+ export declare function findStubTarget(scope: StubScope | undefined, tool: string): {
5
+ scope: StubScope;
6
+ tool: string;
7
+ } | undefined;
8
+ export declare function stubCallId(sessionId: string, turnId: string, callId: string): string;
@@ -0,0 +1 @@
1
+ function stubToolPath(e,t){return e?.agentPath===void 0?t:`${e.agentPath}/${t}`}function findStubTarget(e,t){if(e!==void 0)return t=stubToolPath(e,t),e.rules.some(e=>e.tool===t)?{scope:e,tool:t}:void 0}function stubCallId(e,t,n){return`${e}:${t}:${n}`}export{findStubTarget,stubCallId,stubToolPath};
@@ -0,0 +1,56 @@
1
+ import type { JsonObject, JsonValue } from "#shared/json.js";
2
+ /** Return JSON or immediately throw an error for a matching tool call. */
3
+ export type ToolStubOutcome = {
4
+ readonly response: JsonValue;
5
+ readonly throw?: never;
6
+ } | {
7
+ readonly response?: never;
8
+ readonly throw: {
9
+ readonly message: string;
10
+ /** Defaults to Error. Sets the name, not a custom JavaScript class. */
11
+ readonly name?: string;
12
+ };
13
+ };
14
+ /** Stub outcomes selected by tool name and input fields. */
15
+ export type ToolStub = {
16
+ readonly id: string;
17
+ /** Use list_tasks for a root tool or researcher/list_tasks for a local subagent's tool. */
18
+ readonly tool: string;
19
+ /** Each named input property must exist and satisfy its JSON Schema. */
20
+ readonly match?: Readonly<Record<string, JsonObject | boolean>>;
21
+ } & ({
22
+ readonly outcome: ToolStubOutcome;
23
+ readonly outcomes?: never;
24
+ } | {
25
+ readonly outcome?: never;
26
+ readonly outcomes: readonly [ToolStubOutcome, ...ToolStubOutcome[]];
27
+ });
28
+ export interface StubCall {
29
+ /** Combines the session, turn, and tool call IDs. */
30
+ readonly callId: string;
31
+ readonly tool: string;
32
+ readonly input: unknown;
33
+ readonly persistent?: boolean;
34
+ }
35
+ export type StubResult = {
36
+ readonly kind: "real";
37
+ } | {
38
+ readonly kind: "error";
39
+ readonly error: string;
40
+ } | {
41
+ readonly kind: "stub";
42
+ readonly ruleId: string;
43
+ readonly position: number;
44
+ readonly outcome: ToolStubOutcome;
45
+ };
46
+ /** Rules and workflow routing set by the server after it authorizes tool stubs. */
47
+ export interface StubScope {
48
+ readonly token: string;
49
+ readonly rules: readonly ToolStub[];
50
+ readonly rootSessionId?: string;
51
+ readonly agentPath?: string;
52
+ }
53
+ export declare const STUB_CONTEXT_KEY = "eve.toolStubs";
54
+ export declare const STUB_MATCHES_NAMESPACE = "eve.tool-stubs.matches";
55
+ export declare const STUB_FAILURE_NAMESPACE = "eve.tool-stubs.failure";
56
+ export declare const stubResponseNamespace: (callId: string) => string;
@@ -0,0 +1 @@
1
+ const STUB_CONTEXT_KEY=`eve.toolStubs`,STUB_MATCHES_NAMESPACE=`eve.tool-stubs.matches`,STUB_FAILURE_NAMESPACE=`eve.tool-stubs.failure`,stubResponseNamespace=e=>`eve.tool-stubs.${e}`;export{STUB_CONTEXT_KEY,STUB_FAILURE_NAMESPACE,STUB_MATCHES_NAMESPACE,stubResponseNamespace};
@@ -0,0 +1,6 @@
1
+ import type { ResolvedAgentGraphBundle } from "#runtime/graph.js";
2
+ import type { ToolStub } from "#tool-stubs/types.js";
3
+ export declare class InvalidToolStubTargetError extends Error {
4
+ }
5
+ /** Check compiled tools without running dynamic resolvers or contacting connections. */
6
+ export declare function validateToolStubTargets(rules: readonly ToolStub[], graph: ResolvedAgentGraphBundle): void;
@@ -0,0 +1 @@
1
+ var InvalidToolStubTargetError=class extends Error{};function validateToolStubTargets(e,t){for(let n of e)if(!hasToolPath(t,n.tool.split(`/`)))throw new InvalidToolStubTargetError(`Tool stub "${n.id}" names an unknown or unsupported tool path "${n.tool}".`)}function hasToolPath(e,t){if(t.some(e=>e.length===0))return!1;let n=e.root;for(let[r,i]of t.entries()){let a=n.turnAgent.tools.find(e=>e.name===i);if(r===t.length-1)return a===void 0?n.agent.dynamicToolResolvers.some(e=>e.slug!==`connection_tools`&&(e.extensionNamespace===void 0||i.startsWith(`${e.extensionNamespace}__`)))||n.subagentRegistry.dynamicResolvers.some(e=>e.name===i)||i.includes(`__`)&&((n.agent.dynamicConnectionResolvers?.length??0)>0||n.agent.connections.some(e=>i.startsWith(`${e.connectionName}__`))):a.behavior?.handling?.kind!==`provider-tool`;if(a===void 0)return n.subagentRegistry.dynamicResolvers.some(e=>e.name===i);let o=a.behavior?.handling;if(o?.kind!==`dispatch`||o.target.kind!==`subagent-call`&&o.target.kind!==`self-agent-call`)return!1;n=e.nodesByNodeId.get(o.target.nodeId)}return!1}export{InvalidToolStubTargetError,validateToolStubTargets};
@@ -1 +1 @@
1
- async function executeAskQuestionTool(e,t){"use workflow";return toAskQuestionOutput(await t.ask(toAskQuestionRequest(e)))}function toAskQuestionRequest(e){let t=e.options?.map(e=>({description:e.description,id:e.label,label:e.label}));return{allowFreeform:!0,display:t===void 0?`text`:`select`,prompt:e.question,...t!==void 0&&{options:t}}}function toAskQuestionOutput(e){switch(e.status){case`answered`:return{answer:e.optionId??e.text??``,status:`answered`};case`cancelled`:return{interrupted:!0};case`unavailable`:return{status:`unavailable`}}}executeAskQuestionTool.workflowId=`workflow//eve@0.74.0//executeAskQuestionTool`;export{executeAskQuestionTool,toAskQuestionOutput,toAskQuestionRequest};
1
+ async function executeAskQuestionTool(e,t){"use workflow";return toAskQuestionOutput(await t.ask(toAskQuestionRequest(e)))}function toAskQuestionRequest(e){let t=e.options?.map(e=>({description:e.description,id:e.label,label:e.label}));return{allowFreeform:!0,display:t===void 0?`text`:`select`,prompt:e.question,...t!==void 0&&{options:t}}}function toAskQuestionOutput(e){switch(e.status){case`answered`:return{answer:e.optionId??e.text??``,status:`answered`};case`cancelled`:return{interrupted:!0};case`unavailable`:return{status:`unavailable`}}}executeAskQuestionTool.workflowId=`workflow//eve@0.75.0//executeAskQuestionTool`;export{executeAskQuestionTool,toAskQuestionOutput,toAskQuestionRequest};
@@ -1 +1 @@
1
- import{sleep}from"#compiled/@workflow/core/index.js";async function executeSleepTool(e,t){"use workflow";let n=new Promise(e=>t.abortSignal.addEventListener(`abort`,()=>e(`interrupted`),{once:!0})),r=sleep(Math.ceil(e.seconds*1e3)).then(()=>`elapsed`);return await Promise.race([r,n])===`elapsed`?{waitedSeconds:e.seconds}:{interrupted:!0}}executeSleepTool.workflowId=`workflow//eve@0.74.0//executeSleepTool`;export{executeSleepTool};
1
+ import{sleep}from"#compiled/@workflow/core/index.js";async function executeSleepTool(e,t){"use workflow";let n=new Promise(e=>t.abortSignal.addEventListener(`abort`,()=>e(`interrupted`),{once:!0})),r=sleep(Math.ceil(e.seconds*1e3)).then(()=>`elapsed`);return await Promise.race([r,n])===`elapsed`?{waitedSeconds:e.seconds}:{interrupted:!0}}executeSleepTool.workflowId=`workflow//eve@0.75.0//executeSleepTool`;export{executeSleepTool};
@@ -1,4 +1,4 @@
1
- import { a as conversationProjection, i as openConversationInputs, n as conversationReducer, o as callStatus, r as defaultMessageReducer, t as useEveAgent } from "../chunks/use-eve-agent-DOOXadF0.js";
1
+ import { a as conversationProjection, i as openConversationInputs, n as conversationReducer, o as callStatus, r as defaultMessageReducer, t as useEveAgent } from "../chunks/use-eve-agent-DK_T88EP.js";
2
2
 
3
3
  //#region src/client/tool-call-state.ts
4
4
  function toolCallState(conversation, callId, options = {}) {
@@ -1,3 +1,3 @@
1
- import { t as useEveAgent } from "../chunks/use-eve-agent-DOOXadF0.js";
1
+ import { t as useEveAgent } from "../chunks/use-eve-agent-DK_T88EP.js";
2
2
 
3
3
  export { useEveAgent };
@@ -123,7 +123,7 @@ Note: consider the privacy, confidentiality, and user-experience implications fo
123
123
 
124
124
  A delegated subagent publishes progress on its own child-session stream. When a subagent tool call or a workflow tool's `ctx.agent` opens a session, the parent emits `agent.started` with the call's `callId` and `turnId`, its `taskId` when the call runs as a task, the child's `sessionId`, and a `streamPath` that a client follows with `session.agent(started).stream()`. A subagent tool call is a [task](#task-events) whose `task.started` has `kind: "agent"`: its `task.started` and `task.settled` carry the call, and `task.settled.data.output` is the child's reply. A call that fails before the child session opens has no `agent.started`. A settled call does not end the child session; later calls with the same `taskId` reach it.
125
125
 
126
- A question or sign-in from inside a running call does not end the turn. This covers a workflow tool's `ctx.ask()`, including the built-in `ask_question` tool, and a delegated subagent's question or sign-in. The stream emits `input.requested` or `authorization.required`, then `turn.waiting` with `on: "input"` and the open turn's `turnId`. After the answer or sign-in, the turn resumes with the next `step.started` for the same `turnId`, and only `turn.completed`, `turn.failed`, or `turn.cancelled` ends it. When you answer a question that belongs to a workflow tool or a subagent, the session you answer on emits `input.resolved` for it as it routes the answer down. While other relayed questions are still pending, it then emits `turn.waiting` with `on: "input"` again. If the workflow tool run that relayed a question or approval ends before anyone answers, that session emits `input.resolved` with `outcome: "cancelled"` for it. This covers the run's own `ctx.ask()` questions and requests from sessions it opened with `ctx.agent`. A cancelled turn does the same for every question or approval the session relays. A sign-in or tool approval the turn raises itself holds it the same way, also with `on: "input"`. A message from the same person steers the turn and cancels the request: a sign-in reports `authorization.completed` with `outcome: "declined"`, and an approval reports `input.resolved` with `outcome: "ignored"`. Messages from other people wait until the turn ends. Cancelling the turn ends both: a held sign-in reports `authorization.completed` with `outcome: "declined"`, and a held approval reports `input.resolved` with `outcome: "cancelled"`.
126
+ A question or sign-in from inside a running call does not end the turn. This covers a workflow tool's `ctx.ask()`, including the built-in `ask_question` tool, and a delegated subagent's question or sign-in. The stream emits `input.requested` or `authorization.required`, then `turn.waiting` with `on: "input"` and the open turn's `turnId`. After the answer or sign-in, the turn resumes with the next `step.started` for the same `turnId`, and only `turn.completed`, `turn.failed`, or `turn.cancelled` ends it. When you answer a question that belongs to a workflow tool or a subagent, the session you answer on emits `input.resolved` for it as it routes the answer down. A subagent's tool approval instead stays open until the subagent resolves it, and the session relays that subagent's `input.resolved`. While other relayed questions are still pending, it then emits `turn.waiting` with `on: "input"` again. If the workflow tool run that relayed a question or approval ends before anyone answers, that session emits `input.resolved` with `outcome: "cancelled"` for it. This covers the run's own `ctx.ask()` questions and requests from sessions it opened with `ctx.agent`. A cancelled turn does the same for every question or approval the session relays. A sign-in or tool approval the turn raises itself holds it the same way, also with `on: "input"`. A message from the same person steers the turn and cancels the request: a sign-in reports `authorization.completed` with `outcome: "declined"`, and an approval reports `input.resolved` with `outcome: "ignored"`. Messages from other people wait until the turn ends. Cancelling the turn ends both: a held sign-in reports `authorization.completed` with `outcome: "declined"`, and a held approval reports `input.resolved` with `outcome: "cancelled"`.
127
127
 
128
128
  ### Task events
129
129
 
@@ -219,7 +219,7 @@ Message sends default to `"steer"`. Before assistant output begins, eve interrup
219
219
 
220
220
  If the session is waiting on a human-in-the-loop approval, respond with the channel’s Approve or Cancel controls. Text that doesn't match an option doesn't approve the call. From the person the turn serves, it steers the turn and cancels the approval, even when sent with `turnPolicy: "queue"`, since the turn can't end until that person acts; from anyone else, it waits until the turn ends. If they had already approved some calls in the batch, those calls still run. Cancelling the turn withdraws the approval, and a later answer to it approves nothing.
221
221
 
222
- A pending `ctx.ask()` question from a tool, such as `ask_question`, can be answered with plain text. When it is the only pending question, a message that matches an option answers it, and so does any message when the question allows free text. The stream records that message as `message.received`, carrying its delivery id, just before the `input.resolved` it produces. Otherwise eve does not guess which question the text addresses: the message follows the normal `turnPolicy`. A steering message withdraws the questions of the `execute` workflow tool calls the turn waits on, which resolve as `cancelled`; a task's questions stay open. Questions from subagents always need a structured response. Use structured responses to target requests unambiguously.
222
+ A pending prompt, whether a `ctx.ask()` question from a tool such as `ask_question`, a tool approval, or a session-limit prompt, can be answered with plain text, including prompts proxied from a subagent. A message answers the first open prompt, as described in [Several requests at once](/docs/human-in-the-loop#several-requests-at-once): a reply that matches one of its options, such as `approve` or `continue`, answers it as if the sender had pressed that option, and so does any message when the prompt allows free text. Send one message per prompt to answer several. The stream records a message that answers a question as `message.received`, carrying its delivery id, just before the `input.resolved` it produces. A message that doesn't answer the first open prompt follows the normal `turnPolicy`. A steering message withdraws the questions of the `execute` workflow tool calls the turn waits on, which resolve as `cancelled`; a task's questions stay open. Use structured responses to target requests unambiguously.
223
223
 
224
224
  A structured response matches any currently pending request by ID, not only the newest batch. It becomes stale only after that request was answered, cleared, or cancelled. eve delivers a stale response to the model as a new user message, and the model decides whether the old selection still matters. A stale approval never authorizes the earlier tool call; the model must request the action and approval again if they are still needed.
225
225
 
@@ -1,5 +1,14 @@
1
1
  {
2
2
  "title": "Evals",
3
3
  "defaultOpen": false,
4
- "pages": ["overview", "cases", "assertions", "judge", "targets", "reporters", "running"]
4
+ "pages": [
5
+ "overview",
6
+ "cases",
7
+ "assertions",
8
+ "tool-stubs",
9
+ "judge",
10
+ "targets",
11
+ "reporters",
12
+ "running"
13
+ ]
5
14
  }
@@ -7,6 +7,8 @@ An eval is a scored check that runs your agent against real sessions and grades
7
7
 
8
8
  Evals exercise the same HTTP surface your users hit. The runner boots (or targets) a real agent server, drives sessions through the [TypeScript client](../guides/client/overview) protocol, and grades what comes back, so a passing eval means the agent booted, accepted a request, and produced the result you asserted.
9
9
 
10
+ Define [tool stubs](./tool-stubs) in your eval to control tool results, then assert on the agent's tool calls and responses.
11
+
10
12
  ## `defineEval`
11
13
 
12
14
  eve discovers evals under the app-root `evals/` directory, in `.eval.ts` files. Each file is one eval by default. A file can also default-export an array to fan out over a dataset (see [Cases](./cases)). The file path is the eval's identity, so you don't author an `id` or `name`. Directories group related evals (`evals/weather/brooklyn-forecast.eval.ts` becomes id `weather/brooklyn-forecast`).
@@ -0,0 +1,257 @@
1
+ ---
2
+ title: "Tool Stubs"
3
+ description: "Define tool outcomes and outcome sequences in your evals."
4
+ ---
5
+
6
+ Pass `stubs` when creating an eval session to replace selected tool executions with JSON responses. The same rules work against local and deployed agents, without deploying mock functions or keeping the runner connected to answer tool calls.
7
+
8
+ Stubs specify what tools return. [Assertions](./assertions) check what the agent does and tells the user. Unused stubs produce a warning without failing the eval.
9
+
10
+ ## Author an eval
11
+
12
+ ```ts title="evals/complete-task.eval.ts"
13
+ import { defineEval } from "eve/evals";
14
+
15
+ export default defineEval({
16
+ async test(t) {
17
+ const session = await t.session({
18
+ stubs: [
19
+ {
20
+ id: "tasks",
21
+ tool: "list_tasks",
22
+ outcomes: [
23
+ {
24
+ response: {
25
+ tasks: [
26
+ { id: "milk", title: "Buy milk" },
27
+ { id: "dog", title: "Walk dog" },
28
+ { id: "rent", title: "Pay rent" },
29
+ ],
30
+ },
31
+ },
32
+ {
33
+ response: {
34
+ tasks: [
35
+ { id: "dog", title: "Walk dog" },
36
+ { id: "rent", title: "Pay rent" },
37
+ ],
38
+ },
39
+ },
40
+ ],
41
+ },
42
+ {
43
+ id: "complete-milk",
44
+ tool: "complete_task",
45
+ match: { task_id: { const: "milk" } },
46
+ outcome: { response: { success: true } },
47
+ },
48
+ ],
49
+ });
50
+
51
+ const first = await session.send("What tasks do I have?");
52
+ first.expectOk();
53
+ first.calledTool("list_tasks", { count: 1 });
54
+ first.messageIncludes("Buy milk");
55
+
56
+ const second = await session.send("Complete Buy milk, then list my remaining tasks.");
57
+ second.expectOk();
58
+ second.calledTool("complete_task", { input: { task_id: "milk" }, count: 1 });
59
+ second.toolOrder(["complete_task", "list_tasks"]);
60
+ second.messageIncludes("Walk dog");
61
+ second.messageIncludes("Pay rent");
62
+ },
63
+ });
64
+ ```
65
+
66
+ Use your agent's actual tool names and response shapes. The client accepts the same option on `client.sessions.create({ stubs })` or `client.sessions.create({ message, stubs })`.
67
+
68
+ These responses describe a sequence, not a simulated task database. The second `list_tasks` call returns two tasks even if `complete_task` was never called. The completion and order assertions check that the agent actually requested the write; testing a real database update requires the real tool.
69
+
70
+ ## Stub fields
71
+
72
+ | Field | Meaning |
73
+ | ---------- | -------------------------------------------------------------------------------------------------------------------------------------- |
74
+ | `id` | Your session-unique rule identifier. eve uses it to track this rule's response sequence. It is neither a tool name nor a tool-call ID. |
75
+ | `tool` | The existing tool's name, optionally qualified by its local subagent path. |
76
+ | `match` | Optional input-property constraints, expressed as JSON Schema. Omit it or use `{}` to match any valid input. |
77
+ | `outcome` | One outcome repeated on every matching call, such as `{ response: { tasks: [] } }`. |
78
+ | `outcomes` | A nonempty array of outcomes returned in sequence. Use exactly one of `outcome` or `outcomes`. |
79
+
80
+ Each outcome contains exactly one of `response` or `throw`. `response` holds the JSON returned by the tool; `throw` describes an error raised immediately instead. The enclosing outcome is not part of the tool's response. Top-level `response` and `responses` are not accepted.
81
+
82
+ Exposing rule IDs and response positions in traces is a follow-up: [AX-5157](https://linear.app/vercel/issue/AX-5157).
83
+
84
+ Stubs are plain JSON and can be shared between evals:
85
+
86
+ ```ts title="evals/fixtures/tasks.ts"
87
+ import type { ToolStub } from "eve/evals";
88
+
89
+ export const taskStubs = [
90
+ { id: "tasks", tool: "list_tasks", outcome: { response: { tasks: [] } } },
91
+ ] satisfies readonly ToolStub[];
92
+ ```
93
+
94
+ Pass the fixture to `t.session({ stubs: taskStubs })`. Each session has its own sequence position. Both `eve/evals` and `eve/client` export `ToolStub` and `ToolStubOutcome`. These types check the JSON contract; they do not infer the response type from your tool definition.
95
+
96
+ ## Match tool arguments
97
+
98
+ **Rules are evaluated from top to bottom. The first matching stub wins.** Put specific rules before an unconditional fallback:
99
+
100
+ ```ts
101
+ const stubs = [
102
+ {
103
+ id: "milk",
104
+ tool: "complete_task",
105
+ match: { task_id: { const: "milk" } },
106
+ outcome: { response: { success: true } },
107
+ },
108
+ { id: "other-tasks", tool: "complete_task", outcome: { response: { success: false } } },
109
+ ];
110
+ ```
111
+
112
+ A call for milk receives success; other valid calls receive the fallback. Reversing these rules makes the unconditional rule win every time. If no rule matches, eve executes the real tool. A broken stub or failure processing its response fails the eval, even if the agent recovers. An intentional `throw` outcome is a normal tool failure that the eval can assert on. Neither falls back to real execution.
113
+
114
+ If eve cannot record an output-processing failure, it cancels the workflow serving stubs. The original tool error or fallback is preserved, but verification rejects the eval. Failed or cancelled stub sessions cannot pass verification.
115
+
116
+ Every property listed in `match` must exist and satisfy its constraint. Extra top-level properties are allowed:
117
+
118
+ ```json
119
+ {
120
+ "id": "open-tasks",
121
+ "tool": "search_tasks",
122
+ "match": {
123
+ "filter": {
124
+ "type": "object",
125
+ "properties": { "status": { "const": "open" } },
126
+ "required": ["status"]
127
+ },
128
+ "query": { "type": "string", "pattern": "\\b(?i:milk)\\b" },
129
+ "tags": { "type": "array", "contains": { "const": "urgent" } }
130
+ },
131
+ "outcome": { "response": { "tasks": ["Buy milk"] } }
132
+ }
133
+ ```
134
+
135
+ Nested properties are optional unless listed in `required`. Matching does not coerce values, apply defaults, or change arguments. The real tool schema validates input before replacement; normal approvals and output processing still apply.
136
+
137
+ Constraints use a reference-free subset of JSON Schema 2020-12: types, scalar `const` and `enum`, string patterns and bounds, numeric bounds, array items/contains, object properties/required/additionalProperties, and logical/conditional schemas. `pattern` uses JavaScript regular expressions. Schema validation, regex syntax checks, and validator initialization run before session creation.
138
+
139
+ Unsupported constraints fail setup: `patternProperties`, references, formats, `multipleOf`, `minContains`, `maxContains`, `uniqueItems`, dependencies, and object/array values in `const` or `enum`. Nested `properties` and `required` cannot use inherited property names such as `constructor` or `toString`. Use nested properties or array items to match structured values.
140
+
141
+ Unknown static tool names, invalid local-agent paths, and paths into remote agents fail session creation. Dynamic names and connection operations are checked where possible; unresolved names are allowed. After the eval, unused rule IDs are reported as warnings. Use tool assertions when a call must happen.
142
+
143
+ Configuration is limited to 100 rules, 20,000 JSON nodes, nesting depth 32, and approximately one million characters. The `__proto__` property name is reserved.
144
+
145
+ ## Sequences and session scope
146
+
147
+ `outcome` repeats on every matching call. `outcomes` advances on each new matching call, then repeats its final entry. For example, `outcomes: [{ response: "pending" }, { response: "completed" }]` returns `"pending"` once and `"completed"` thereafter. Only the selected rule advances; exhaustion never selects a later rule.
148
+
149
+ Two calls within one agent turn consume two entries. Later turns continue the same sequence. Calls awaiting or denied approval do not consume entries. Each session supports up to 10,000 recorded calls.
150
+
151
+ Choose the rules at session creation; they stay fixed for that session. Local subagents share playback through explicit tool paths, and deployment handoffs preserve it. An independent session starts at the first response.
152
+
153
+ eve's transparent internal workflow replay reuses the recorded outcome without consuming another entry.
154
+
155
+ Parallel calls consume responses in arrival order, which can vary. Use separate argument matchers when particular arguments must receive particular responses.
156
+
157
+ ## Permit tool replacement
158
+
159
+ Tool replacement is denied by default, including in development. With [Vercel OIDC](../guides/auth-and-route-protection#verceloidc), put the permission on the eval runner's verified subject:
160
+
161
+ ```ts title="agent/channels/eve.ts"
162
+ import { vercelOidc, vercelSubject } from "eve/channels/auth";
163
+ import { eveChannel } from "eve/channels/eve";
164
+
165
+ export default eveChannel({
166
+ auth: [
167
+ vercelOidc({
168
+ subjects: [
169
+ vercelSubject({ teamSlug: "acme", projectName: "app" }),
170
+ {
171
+ subject: vercelSubject({ teamSlug: "acme", projectName: "eval-runner" }),
172
+ allowToolStubs: true,
173
+ },
174
+ ],
175
+ }),
176
+ ],
177
+ });
178
+ ```
179
+
180
+ Both projects can authenticate; only the eval runner receives replacement permission. `vercelSubject` defaults to production; pass `environment: "*"` to include all environments. The target's server configuration supplies the grant after token verification. Request JSON cannot grant permission, and user principals do not inherit project grants.
181
+
182
+ Custom authenticators can return the same `allowToolStubs: true` after verifying the caller. See [authentication and route protection](../guides/auth-and-route-protection#tool-replacement-permission) for the grant rules and a local development example.
183
+
184
+ Permission is checked at session creation before identity projection. Later messages and result reads use normal channel authentication. Anyone your channel admits to an existing session can use its stubs. Your application's session-access policy must provide the required isolation; an isolated eval deployment can use the same agent code.
185
+
186
+ The stub verification endpoint also requires `allowToolStubs`, checked before reading workflow status.
187
+
188
+ ## Supported tools and subagents
189
+
190
+ Stubs replace execution of existing ordinary, dynamic, and workflow tools. They retain the real tool's input schema, approvals, and output processing. A stub does not register a new tool or make it visible to the model.
191
+
192
+ Ordinary tools with configured stub rules can emit an extra `action.partial` containing the final response. This also applies when no rule matches and the real tool returns a non-streaming result. The final `action.result` remains correct, and real streaming still delivers progress. Use `action.result` for final-result assertions. Removing the extra event is tracked in [AX-5164](https://linear.app/vercel/issue/AX-5164).
193
+
194
+ ### Subagents
195
+
196
+ Stubbing a delegation tool returns its configured result without starting the subagent. Persistent tools implemented with `serve`, including the built-in `agent`, require an unconditional stub: omit `match` or use `{}`. Mixing real and mocked calls to the same persistent tool is unsupported.
197
+
198
+ To let a local subagent run but replace its tools, use `/` to qualify the delegation path:
199
+
200
+ ```ts
201
+ const stubs = [
202
+ { id: "root-tasks", tool: "list_tasks", outcome: { response: ["Root task"] } },
203
+ { id: "research-tasks", tool: "researcher/list_tasks", outcome: { response: ["Research task"] } },
204
+ { id: "nested-tasks", tool: "researcher/assistant/list_tasks", outcome: { response: [] } },
205
+ ];
206
+ ```
207
+
208
+ `researcher` replaces the whole delegation; `researcher/list_tasks` replaces only that subagent's list tool. The built-in `agent` uses `agent/list_tasks`. Model-visible names stay unchanged. A root rule never implicitly matches a subagent's same-named tool.
209
+
210
+ Repeated subagent sessions at the same path share that rule's sequence. Different paths use different rules. Remote agents do not receive the configuration; replace their whole delegation tool instead.
211
+
212
+ ### Connections and provider tools
213
+
214
+ Use the qualified operation name, such as `linear__list_issues` or `researcher/linear__list_issues`. Put operation arguments in `match` and its JSON result in `outcome.response`, or in each sequence entry's `response`.
215
+
216
+ Connection authentication and discovery stay live so eve can validate the operation and its arguments. Stubs do not provide an offline connection. The framework's `connection_search` and `connection_execute` tools cannot themselves be replaced.
217
+
218
+ Tools executed by the model provider, such as provider-hosted web search, have no eve executor to replace. A search tool implemented in your own agent can be stubbed.
219
+
220
+ ## Inject tool errors
221
+
222
+ Use `throw` to test how the agent handles a failed tool call. Mix errors and responses to test recovery:
223
+
224
+ ```ts
225
+ const stubs = [
226
+ {
227
+ id: "tasks",
228
+ tool: "list_tasks",
229
+ outcomes: [
230
+ { throw: { name: "TimeoutError", message: "Task service timed out" } },
231
+ { response: { tasks: [] } },
232
+ ],
233
+ },
234
+ ];
235
+ ```
236
+
237
+ `throw.message` is required. `throw.name` is optional and defaults to `"Error"`. eve creates an ordinary JavaScript `Error` with that name and message; it does not instantiate custom error classes. `TimeoutError` reports a timeout immediately, without delaying the call or testing an actual deadline.
238
+
239
+ Ordinary tools and connection operations expose the error message to the model, without the error name. Workflow task errors also retain the name. Put any details the model needs to act on in `message`.
240
+
241
+ Use `outcome: { throw: { message: "Service unavailable" } }` to fail every matching call. A sequence consumes one entry per new call, whether it returns or throws, and repeats its final entry. Workflow replay reuses the same recorded outcome.
242
+
243
+ An injected exception follows the tool's normal error handling. Ordinary and blocking workflow calls are recorded as failed; background tasks report failure when they settle. The model can report the failure or try again. Assert the failed call explicitly:
244
+
245
+ ```ts
246
+ const turn = await session.send(
247
+ "List my tasks once. If the service fails, explain the failure and wait for me to retry.",
248
+ );
249
+ turn.expectOk();
250
+ turn.calledTool("list_tasks", { status: "failed", count: 1 });
251
+ ```
252
+
253
+ The eval can pass when the agent handles the failure correctly. `noFailedActions()` still rejects the failed call, so use explicit assertions for expected failures. Stub infrastructure and response-processing failures still fail verification.
254
+
255
+ This works for ordinary tools, workflow tools, whole-agent replacements, and qualified connection operations. An exception in a persistent workflow tool ends that task; retry with a new call, rather than its failed task ID. Normal approvals apply before an outcome is consumed.
256
+
257
+ Returning `{ response: { error: "timeout" } }` remains ordinary JSON. Only `throw` raises an exception.
@@ -178,7 +178,7 @@ A mount gives the extension's contributions a namespace. Updating the package up
178
178
 
179
179
  ### Install the package
180
180
 
181
- Install the extension with the package manager already used by the consumer's agent project. Fresh eve projects use pnpm:
181
+ Install the extension with the package manager already used by the consumer's agent project. For example, in a pnpm project:
182
182
 
183
183
  ```bash
184
184
  pnpm add @acme/crm
@@ -113,6 +113,8 @@ Verifies a bearer JWT against the [Vercel OIDC issuer](https://vercel.com/docs/o
113
113
 
114
114
  Auth fails closed: routes reject unauthenticated traffic by default, and the OIDC user branch verifies `external_sub` against `VERCEL_PROJECT_ID` and the deployment environment, returning `false` when either is unset. An external-subject token cannot authenticate on a deployment that hasn't pinned its project.
115
115
 
116
+ Subject entries can also be objects: `{ subject: vercelSubject({ teamSlug: "acme", projectName: "eval-runner" }), allowToolStubs: true }`. This explicitly permits that verified service/runtime caller to create [stubbed eval sessions](../evals/tool-stubs). Plain subject strings and implicit current-project acceptance grant ordinary access only. Custom authenticators can return the same permission in their authentication result; it is not part of persisted or forwarded session identity.
117
+
116
118
  #### `subjects` patterns and `vercelSubject(...)`
117
119
 
118
120
  Each `subjects` entry is matched against the token's `sub` claim, which Vercel shapes as `owner:<team>:project:<name>:environment:<env>`. Hand-writing that string is a footgun: a typo silently rejects every caller, and an over-broad `*` wildcard silently lets unrelated ones in. Build the pattern with `vercelSubject(...)` instead. It rejects malformed input at construction time, and defaults `environment` to `"production"` when you omit it, so an unspecified environment cannot silently accept preview or development tokens:
@@ -298,6 +300,29 @@ Use the principal on `auth.current` (or `auth.initiator`) to scope tools, resolv
298
300
 
299
301
  Route auth does not enforce session ownership. If multiple users or tenants can reach the same route, you must implement the per-user, per-tenant, or per-session authorization your application requires.
300
302
 
303
+ ## Tool replacement permission
304
+
305
+ Creating a [stubbed eval session](../evals/tool-stubs) requires an explicit `allowToolStubs: true` in the verified route authentication result. The target's server configuration grants permission; request JSON cannot. With `vercelOidc`, put the grant on a `subjects` entry as shown above. Any matching entry with the grant permits replacement; other entries do not revoke it. Implicit current-project acceptance and user principals do not inherit project grants.
306
+
307
+ Custom authenticators return the same grant after verifying their caller. For example, this alternative channel permits stubs only in local development:
308
+
309
+ ```ts
310
+ import { localDev } from "eve/channels/auth";
311
+ import { eveChannel } from "eve/channels/eve";
312
+
313
+ const authenticate = localDev();
314
+ export default eveChannel({
315
+ auth: async (request) => {
316
+ const caller = await authenticate(request);
317
+ return caller ? { ...caller, allowToolStubs: true } : null;
318
+ },
319
+ });
320
+ ```
321
+
322
+ The channel checks the grant before forwarded-principal handling or `onMessage` projection. It removes the grant before using the identity for forwarding, audience classification, or session persistence. This is an authentication-result permission, not a session attribute.
323
+
324
+ Later messages, approvals, controls, and result reads use normal channel authentication. There is no additional creator-only rule or replacement-permission recheck. Anyone your channel admits to an existing session can use its configured stubs; apply the same application-specific session-access policy you need for ordinary sessions.
325
+
301
326
  ## Tool and connection auth
302
327
 
303
328
  Tool and connection auth is how your agent reaches an external service that wants an interactive sign-in, like an OAuth MCP server. Connections declare `auth` on the connection definition. Tools should resolve providers inline with `ctx.getToken(provider)` and call `ctx.requireAuth(provider)` only when a downstream service rejects a token; eve drives the sign-in, caches the token per step, and re-runs the call once the caller authorizes.
@@ -221,9 +221,16 @@ same name; namespace one map key to remove the ambiguity.
221
221
 
222
222
  Dynamic connections support `session.started` and `turn.started`. A turn result
223
223
  replaces that file's session result for the turn, including when the turn
224
- handler returns `null`. A throwing or invalid handler fails the lifecycle
225
- without rebuilding the registry, so a static connection shadowed by the
226
- dynamic result cannot reappear as a fallback.
224
+ handler returns `null`. A throwing or invalid handler fails the current turn
225
+ and parks the session, so a later message can retry after the dependency
226
+ recovers. The failed lifecycle does not rebuild the registry, so a static
227
+ connection shadowed by the dynamic result cannot reappear as a fallback.
228
+
229
+ Recovery needs another message. eve does not keep the input from the failed
230
+ turn or replay it, so the caller must send it again once the dependency is
231
+ back. This applies to the first message of a session too. A scheduled run
232
+ whose resolver fails is not retried; the session stays idle until the next
233
+ scheduled run or message arrives.
227
234
 
228
235
  eve may run the active session and turn handlers again when a parked turn
229
236
  resumes or a durable step retries. This rebuilds live auth, header, approval,