Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions docs/guides/client/messages.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,14 @@ const resumed = await session.send({
await resumed.result();
```

For response-authorized approvals, submitting `approve` creates a responder-bound candidate; it does not immediately close the shared request. Keep consuming the durable stream:

- `approval.candidate` reports safe pending, rejected, failed, timed-out, and stale outcomes. Candidate-local UI should be private to its responder.
- candidate-correlated `authorization.required` carries a private sign-in challenge.
- `approval.settled` is the terminal authority for removing shared controls and shows whether the request was approved or cancelled.

Delivery only acknowledges ingestion. OAuth may resume the same candidate later, so reconnecting clients should replay settlement history to repair stale UI.

You can send `message`, `inputResponses`, and `clientContext` together when the resumed turn needs both a human answer and follow-up text.

## Single-use responses
Expand Down
27 changes: 27 additions & 0 deletions docs/tools/human-in-the-loop.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,33 @@ Policies can also return `"approved"` or `"denied"` to decide automatically. Use

Gating a side effect on approval is also how you make non-idempotent work safe across replays: a charge or email that sits behind `always()` can't fire from a re-run step without a fresh human decision.

### Authorize who may approve

Request-time approval decides whether a tool call prompts. To also authorize the person responding, use the object form:

```ts
approval: {
policy: always(),
async authorizeResponse({ request, responder, session, auth }) {
const identity = await auth.getToken(approverAuth);
return await canApprove({ identity, request, responder, session })
? "allowed"
: {
status: "rejected",
safeReason: "You are not permitted to approve this action.",
};
},
},
```

`request` contains the stable request, call, tool name, and tool input. `responder` is the identity derived by the authenticated channel ingress. `session` exposes read-only identity and lineage. `auth` exposes only `getToken` and `requireAuth`; it has no sandbox, skill, model, channel, or tool-execution capability.

Calling `auth.getToken()` uses the same durable `authorization.required` flow as tool execution. If sign-in is needed, eve keeps the shared approval pending, binds the private challenge to that responder, and re-runs the currently deployed authorizer after callback. Return an authored `safeReason` only when it is safe to show privately to the responder. Thrown errors and the ten-second authorizer timeout fail closed with generic retry feedback.

Approval controls are **Approve / Cancel**. Approve runs `authorizeResponse`. Cancel means “do not run this proposed call” and uses the channel's ordinary authenticated interaction boundary without running the authorizer. Multiple responders may validate concurrently; the first approved candidate or Cancel settles atomically, and late candidates become stale.

The stream exposes `approval.candidate` progress and terminal `approval.settled` events. Candidate submission acknowledges ingestion only; clients keep shared controls open until settlement. Credentials, OAuth URLs, and raw provider errors are not included in candidate audit history.

### Skipping approval for schedule-dispatched turns

`session.auth.current` identifies the caller of this turn. Markdown schedules use the app principal (`authenticator: "app"`, `principalId: "eve:app"`, `principalType: "runtime"`) automatically. A `run` schedule must pass its `appAuth` to `receive(...)` for the child session to use that principal. Match all three fields to skip approval for automated turns while still prompting when a person calls the same tool:
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { defineDynamic, defineInstructions } from "#public/instructions/index.js";

export default defineDynamic({
events: {
"session.started": () =>
defineInstructions({
markdown: "Answer with evidence from the current session.",
}),
},
});
11 changes: 11 additions & 0 deletions packages/eve/extension-contracts/compatibility/dynamicSkill/v3.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { defineDynamic, defineSkill } from "#public/skills/index.js";

export default defineDynamic({
events: {
"session.started": () =>
defineSkill({
description: "Triage incoming requests.",
markdown: "# Triage\n\nInspect the request before acting.",
}),
},
});
12 changes: 12 additions & 0 deletions packages/eve/extension-contracts/compatibility/dynamicTool/v7.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { defineDynamic, defineTool } from "#public/tools/index.js";

export default defineDynamic({
events: {
"session.started": () =>
defineTool({
description: "Return the current status.",
inputSchema: { type: "object", properties: {} },
execute: () => ({ status: "ready" }),
}),
},
});
12 changes: 12 additions & 0 deletions packages/eve/extension-contracts/compatibility/hook/v4.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { defineHook } from "#public/hooks/index.js";

export default defineHook({
events: {
"input.requested"(event) {
console.info(
"input requested",
event.data.requests.map((request) => request.requestId),
);
},
},
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"kind": "eve-extension-capability-contract",
"capability": "dynamicInstructions",
"epoch": 4,
"sha256": "bc663add7df2d5f5a81b8d796ff24012fa56c6b3e874a9d322d918aefb711008",
"exports": ["defineDynamic"]
}
7 changes: 7 additions & 0 deletions packages/eve/extension-contracts/reports/dynamicSkill/v4.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"kind": "eve-extension-capability-contract",
"capability": "dynamicSkill",
"epoch": 4,
"sha256": "bc663add7df2d5f5a81b8d796ff24012fa56c6b3e874a9d322d918aefb711008",
"exports": ["defineDynamic"]
}
13 changes: 13 additions & 0 deletions packages/eve/extension-contracts/reports/dynamicTool/v8.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
{
"kind": "eve-extension-capability-contract",
"capability": "dynamicTool",
"epoch": 8,
"sha256": "ec3389a248b942d9080056c17747a08556abf4b1addf185ad803ba27edb735ef",
"exports": [
"DynamicToolEntry",
"DynamicToolEvents",
"DynamicToolResult",
"DynamicToolSet",
"defineDynamic"
]
}
7 changes: 7 additions & 0 deletions packages/eve/extension-contracts/reports/hook/v5.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"kind": "eve-extension-capability-contract",
"capability": "hook",
"epoch": 5,
"sha256": "4b994ceb0e64318c40afbf17de07e09b059822a20a9eb5c0a92d7a9c86832535",
"exports": ["defineHook"]
}
8 changes: 4 additions & 4 deletions packages/eve/src/compiler/extension-compatibility.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,13 +22,13 @@ interface ExtensionCapabilityContract {
const EXTENSION_CAPABILITY_CONTRACTS = {
extension: { current: 1, supported: [1], dropped: {} },
tool: { current: 4, supported: [1, 2, 3, 4], dropped: {} },
dynamicTool: { current: 7, supported: [1, 2, 3, 4, 5, 6, 7], dropped: {} },
dynamicTool: { current: 8, supported: [1, 2, 3, 4, 5, 6, 7, 8], dropped: {} },
connection: { current: 3, supported: [1, 2, 3], dropped: {} },
hook: { current: 4, supported: [1, 2, 3, 4], dropped: {} },
hook: { current: 5, supported: [1, 2, 3, 4, 5], dropped: {} },
skill: { current: 1, supported: [1], dropped: {} },
dynamicSkill: { current: 3, supported: [1, 2, 3], dropped: {} },
dynamicSkill: { current: 4, supported: [1, 2, 3, 4], dropped: {} },
instructions: { current: 1, supported: [1], dropped: {} },
dynamicInstructions: { current: 3, supported: [1, 2, 3], dropped: {} },
dynamicInstructions: { current: 4, supported: [1, 2, 3, 4], dropped: {} },
config: { current: 1, supported: [1], dropped: {} },
state: { current: 2, supported: [1, 2], dropped: {} },
} as const satisfies Record<string, ExtensionCapabilityContract>;
Expand Down
Loading