@omercnet/paseo-omp 4.0.0 → 5.0.0-next.320.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/OVERVIEW.md +1 -1
- package/README.md +10 -7
- package/npm-shrinkwrap.json +15 -15
- package/package.json +8 -8
- package/paseo-plugin.json +1 -1
- package/server/package-version.ts +1 -1
- package/server/provider/connection.ts +17 -0
- package/server/provider/omp-rpc-protocol.ts +82 -5
- package/server/provider/omp-rpc.ts +21 -1
- package/server/provider/registration.ts +1 -0
- package/server/provider/session-history.ts +4 -0
- package/server/provider/session-permissions.ts +227 -35
- package/server/provider/session.ts +32 -3
package/OVERVIEW.md
CHANGED
|
@@ -2,7 +2,7 @@ OMP adds an agent provider for the OMP coding agent, plus an OMP sidebar and wor
|
|
|
2
2
|
|
|
3
3
|
## Setup
|
|
4
4
|
|
|
5
|
-
- Paseo ^0.
|
|
5
|
+
- Paseo ^0.14.0.
|
|
6
6
|
- OMP 18.1.15 or newer on the daemon host, speaking RPC protocol v2. Older runtimes are rejected.
|
|
7
7
|
- Provider options let you change the OMP command, set environment values, choose an output redaction mode and set a session directory and timeout. Named OMP profiles appear as separate `OMP · <profile>` providers.
|
|
8
8
|
|
package/README.md
CHANGED
|
@@ -24,7 +24,7 @@ before capture.
|
|
|
24
24
|
|
|
25
25
|
## Quick start
|
|
26
26
|
|
|
27
|
-
Requirements: Paseo `^0.
|
|
27
|
+
Requirements: Paseo `^0.14.0`, OMP `18.1.15` or newer, and OMP RPC protocol v2. Workspace discovery passes `includeBackground`, which throws on hosts that do not advertise `features.backgroundWorkspaces`; every 0.14 host does.
|
|
28
28
|
|
|
29
29
|
```bash
|
|
30
30
|
paseo plugin install npm:@omercnet/paseo-omp@<version>
|
|
@@ -40,7 +40,7 @@ Open the **OMP** sidebar to review the provider-profile contract, choose which c
|
|
|
40
40
|
- [Core-provider issue and parity audit](docs/core-provider-issue-audit.md)
|
|
41
41
|
- [Release checklist](docs/release-checklist.md)
|
|
42
42
|
|
|
43
|
-
The plugin uses public Paseo 0.
|
|
43
|
+
The plugin uses public Paseo 0.14 provider and client contracts and registers distinct `omp-plugin` and `omp-plugin-<profile>` identities; it does not modify the bundled `omp` provider.
|
|
44
44
|
|
|
45
45
|
## Named OMP profiles
|
|
46
46
|
|
|
@@ -54,20 +54,20 @@ Profile selection is request-local: concurrent clients cannot change each other'
|
|
|
54
54
|
|
|
55
55
|
## Paseo provider SDK coverage
|
|
56
56
|
|
|
57
|
-
The capability inventory is pinned to `@getpaseo/plugin` 0.
|
|
57
|
+
The capability inventory is pinned to `@getpaseo/plugin` 0.14.0-beta.1, with nightly compatibility checks
|
|
58
58
|
against the latest and beta channels. Provider capability scoring covers
|
|
59
|
-
all
|
|
59
|
+
all 18 entries in the SDK's `PROVIDER_CAPABILITIES`; the separate provider/client SDK table below
|
|
60
60
|
also tracks the registration, screen, and sidebar APIs introduced in 0.11. This measures strict SDK surface
|
|
61
61
|
coverage, not general product quality.
|
|
62
62
|
|
|
63
|
-
**Provider capability completeness:
|
|
63
|
+
**Provider capability completeness: 63.9%.** The provider advertises 12 of 18 capabilities (66.7%); eleven are complete and `session.configure` is partial.
|
|
64
64
|
|
|
65
65
|
Scoring is deliberately mechanical so releases remain comparable:
|
|
66
66
|
|
|
67
67
|
- **100%**: advertised, implemented end to end, and covered by automated provider tests.
|
|
68
68
|
- **50%**: advertised with a meaningful subset, but at least one material SDK operation is rejected.
|
|
69
69
|
- **0%**: not advertised; requests are rejected rather than silently approximated.
|
|
70
|
-
- Overall completeness is the unweighted arithmetic mean of all
|
|
70
|
+
- Overall completeness is the unweighted arithmetic mean of all 18 SDK capability rows.
|
|
71
71
|
|
|
72
72
|
| SDK capability | Completeness | Support and gap |
|
|
73
73
|
| --- | ---: | --- |
|
|
@@ -77,6 +77,7 @@ Scoring is deliberately mechanical so releases remain comparable:
|
|
|
77
77
|
| `prompt.output_schema` | **0%** | Not advertised. OMP has no equivalent structured-output contract; `outputSchema` fails visibly. |
|
|
78
78
|
| `prompt.steer` | **100%** | Native steering with turn ownership, user-message correlation, stale-target rejection, and terminal race handling. |
|
|
79
79
|
| `session.archive` | **0%** | Not advertised. Paseo may archive its agent record, but OMP has no native transcript archive operation. |
|
|
80
|
+
| `session.can_stop_runtime` | **100%** | Answers Paseo's idle-release safety query. Returns `false` while a turn, permission, revert, configuration change, runtime recovery, active subagent, or OMP background async work is pending, and when OMP does not report a settled state; errors and unknown sessions fail the request, which also keeps the runtime. |
|
|
80
81
|
| `session.configure` | **50%** | Live model and thinking changes commit and republish native state. Live approval-mode changes require a new session, and non-empty SDK `settings` are rejected. |
|
|
81
82
|
| `session.list` | **100%** | Bounded host-wide and cwd-scoped discovery, search, previews, profile-specific session roots, and cleanup-reservation guards. |
|
|
82
83
|
| `session.persistence` | **100%** | Versioned opaque handles, exact resume, import, replay-before-ready, runtime recovery, and cleanup quarantine. Archived agents whose recorded workspace has been removed load read-only from their authorized transcript when Paseo opens them with its `history` purpose, without spawning OMP or connecting MCP servers; interactive create and resume still require the workspace. |
|
|
@@ -125,7 +126,9 @@ Existing agents whose provider is `omp` remain owned by the bundled provider. Ne
|
|
|
125
126
|
|
|
126
127
|
OMP `18.1.15` is the oldest version tested end to end. The direct provider's hard compatibility gate is `rpc-ui` protocol v2: metadata-free legacy ready frames and v1-only runtimes are rejected before a provider session opens because they cannot support the advertised persistence and conversation-rewind capabilities. Typed tool approvals remain capability-gated and fall back as described above. For terminal completion, a matching `agent_end.requestId` is authoritative and mismatches are ignored before state changes. Released OMP 18.2.x builds that omit that field use an ordered fallback requiring a fresh correlated native user entry followed by current-turn assistant activity, an idle non-compacting runtime, and no conflicting permission, tool, steer, or child-session work. Ambiguous events while OMP is active are ignored; unresolved ambiguity after confirmed idle fails only the Paseo turn. This trades a bounded residual same-agent stale-event risk for compatibility with released OMP instead of terminating and lazily restarting the runtime after every later prompt. Paseo clients, including mobile clients inside reconnect grace, do not own or terminate the daemon-managed OMP session.
|
|
127
128
|
|
|
128
|
-
Paseo 0.
|
|
129
|
+
Paseo 0.14 is the minimum supported release. The controlled Docker canary is pinned to official 0.14.0-beta.1; nightly SDK checks cover the latest and beta channels. The canary preserves nested provider-subagent ancestry and spawning-tool links, and validates npm-managed plugin sources, platform-owned external URL opening, and whole-item timeline transforms before Overview grouping.
|
|
130
|
+
|
|
131
|
+
Paseo releases idle resumable agent runtimes after one hour; the agent stays listed as `closed` and the next prompt resumes the same OMP session. The plugin vetoes that release while the session has a running turn, a pending permission, an active subagent, or OMP background async work, and while OMP does not report a settled state (the plugin needs the `hasPendingAsyncWork` and `isSettled` fields in OMP's `get_state`; builds without them are never released). Set `agents.idleRuntimeCleanup: false` in the Paseo config to disable idle release for every provider.
|
|
129
132
|
|
|
130
133
|
Install, update, disable, or remove `paseo-omp` independently of the bundled provider. Verify the provider snapshot contains `omp-plugin` after installation; a bundled `omp` entry may remain present and is not modified by this plugin.
|
|
131
134
|
|
package/npm-shrinkwrap.json
CHANGED
|
@@ -1,22 +1,22 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@omercnet/paseo-omp",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "5.0.0",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "@omercnet/paseo-omp",
|
|
9
|
-
"version": "
|
|
9
|
+
"version": "5.0.0",
|
|
10
10
|
"dependencies": {
|
|
11
|
-
"@getpaseo/protocol": "0.
|
|
11
|
+
"@getpaseo/protocol": "0.14.0-beta.1",
|
|
12
12
|
"@modelcontextprotocol/sdk": "1.30.0",
|
|
13
13
|
"yaml": "^2.9.0"
|
|
14
14
|
}
|
|
15
15
|
},
|
|
16
16
|
"node_modules/@getpaseo/protocol": {
|
|
17
|
-
"version": "0.
|
|
18
|
-
"resolved": "https://registry.npmjs.org/@getpaseo/protocol/-/protocol-0.
|
|
19
|
-
"integrity": "sha512-
|
|
17
|
+
"version": "0.14.0-beta.1",
|
|
18
|
+
"resolved": "https://registry.npmjs.org/@getpaseo/protocol/-/protocol-0.14.0-beta.1.tgz",
|
|
19
|
+
"integrity": "sha512-qkZCmLlL5EQ1PkN0Jy8JLIfyqoj34UC2HdJIicERQ/OdrYGQ3krpOfIpYJ3c3BU27GcDL/E4aoA0GbuEVR4qUw==",
|
|
20
20
|
"dependencies": {
|
|
21
21
|
"semver": "^7.8.5",
|
|
22
22
|
"zod": "^4.4.3"
|
|
@@ -606,9 +606,9 @@
|
|
|
606
606
|
}
|
|
607
607
|
},
|
|
608
608
|
"node_modules/hono": {
|
|
609
|
-
"version": "4.13.
|
|
610
|
-
"resolved": "https://registry.npmjs.org/hono/-/hono-4.13.
|
|
611
|
-
"integrity": "sha512-
|
|
609
|
+
"version": "4.13.14",
|
|
610
|
+
"resolved": "https://registry.npmjs.org/hono/-/hono-4.13.14.tgz",
|
|
611
|
+
"integrity": "sha512-yhRLQ21U6qa9Hsu2N7Qcbh8tE+WKEcoErjIo56LeSGiGXGp9sA7UpE86Ui9uexb61KakiGwuHMn2QdV7w6i6kQ==",
|
|
612
612
|
"license": "MIT",
|
|
613
613
|
"engines": {
|
|
614
614
|
"node": ">=16.9.0"
|
|
@@ -657,9 +657,9 @@
|
|
|
657
657
|
"license": "ISC"
|
|
658
658
|
},
|
|
659
659
|
"node_modules/ip-address": {
|
|
660
|
-
"version": "10.7.
|
|
661
|
-
"resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.7.
|
|
662
|
-
"integrity": "sha512-
|
|
660
|
+
"version": "10.7.5",
|
|
661
|
+
"resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.7.5.tgz",
|
|
662
|
+
"integrity": "sha512-t2v0rkILYv9WXmSL7TpAaU3HXgaFYOPUcH3QHgwQ++gpJuH3bqyMy4Fvt/DJCTKwzH3Cf+bAp5W/mFL39KPLzA==",
|
|
663
663
|
"license": "MIT",
|
|
664
664
|
"engines": {
|
|
665
665
|
"node": ">= 12"
|
|
@@ -902,9 +902,9 @@
|
|
|
902
902
|
}
|
|
903
903
|
},
|
|
904
904
|
"node_modules/qs": {
|
|
905
|
-
"version": "6.
|
|
906
|
-
"resolved": "https://registry.npmjs.org/qs/-/qs-6.
|
|
907
|
-
"integrity": "sha512-
|
|
905
|
+
"version": "6.17.0",
|
|
906
|
+
"resolved": "https://registry.npmjs.org/qs/-/qs-6.17.0.tgz",
|
|
907
|
+
"integrity": "sha512-5LK5BZ2XDHMvcUJaf0CQ5HL3aVgKbpklvByC7qLSxYkfE8COEEbJg5xCHWCTqzqi0Gs4Jn5Mv5v3R38W6edscA==",
|
|
908
908
|
"license": "BSD-3-Clause",
|
|
909
909
|
"dependencies": {
|
|
910
910
|
"es-define-property": "^1.0.1",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@omercnet/paseo-omp",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "5.0.0-next.320.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Paseo integration for OMP, including its direct provider and workspace tooling.",
|
|
6
6
|
"license": "MIT",
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
"publishConfig": {
|
|
17
17
|
"access": "public"
|
|
18
18
|
},
|
|
19
|
-
"packageManager": "bun@1.4.
|
|
19
|
+
"packageManager": "bun@1.4.4",
|
|
20
20
|
"engines": {
|
|
21
21
|
"node": ">=22.18.0"
|
|
22
22
|
},
|
|
@@ -53,16 +53,16 @@
|
|
|
53
53
|
"typecheck": "tsc --noEmit -p tsconfig.client.json && tsc --noEmit -p tsconfig.server.json && tsc --noEmit -p tsconfig.tests.json"
|
|
54
54
|
},
|
|
55
55
|
"dependencies": {
|
|
56
|
-
"@getpaseo/protocol": "0.
|
|
56
|
+
"@getpaseo/protocol": "0.14.0-beta.1",
|
|
57
57
|
"@modelcontextprotocol/sdk": "1.30.0",
|
|
58
58
|
"yaml": "^2.9.0"
|
|
59
59
|
},
|
|
60
60
|
"devDependencies": {
|
|
61
61
|
"@biomejs/biome": "^2.5.10",
|
|
62
|
-
"@getpaseo/cli": "0.
|
|
63
|
-
"@getpaseo/client": "0.
|
|
64
|
-
"@getpaseo/plugin": "0.
|
|
65
|
-
"@getpaseo/server": "0.
|
|
62
|
+
"@getpaseo/cli": "0.14.0-beta.1",
|
|
63
|
+
"@getpaseo/client": "0.14.0-beta.1",
|
|
64
|
+
"@getpaseo/plugin": "0.14.0-beta.1",
|
|
65
|
+
"@getpaseo/server": "0.14.0-beta.1",
|
|
66
66
|
"@oh-my-pi/pi-agent-core": "18.10.1",
|
|
67
67
|
"@oh-my-pi/pi-coding-agent": "18.10.1",
|
|
68
68
|
"@tanstack/react-query": "^5.102.3",
|
|
@@ -70,9 +70,9 @@
|
|
|
70
70
|
"@types/react": "^19.2.18",
|
|
71
71
|
"@vitest/coverage-v8": "^5.0.0",
|
|
72
72
|
"esbuild": "0.28.2",
|
|
73
|
+
"pino": "10.3.1",
|
|
73
74
|
"react": "19.1.0",
|
|
74
75
|
"react-native": "0.81.5",
|
|
75
|
-
"pino": "10.3.1",
|
|
76
76
|
"tsx": "^4.20.6",
|
|
77
77
|
"typescript": "^7.0.2",
|
|
78
78
|
"vitest": "^5.0.0",
|
package/paseo-plugin.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"id": "paseo-omp",
|
|
3
3
|
"description": "Paseo integration for OMP, including its direct provider and workspace tooling.",
|
|
4
|
-
"requirements": { "paseo": "^0.
|
|
4
|
+
"requirements": { "paseo": "^0.14.0" },
|
|
5
5
|
"build": [["node", "scripts/prepare-dependencies.mjs"]]
|
|
6
6
|
}
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
/** Release Please keeps this bundle-safe constant aligned with package.json. */
|
|
2
|
-
export const PASEO_OMP_PACKAGE_VERSION = "
|
|
2
|
+
export const PASEO_OMP_PACKAGE_VERSION = "5.0.0-next.320.1"; // x-release-please-version
|
|
@@ -48,6 +48,7 @@ const SUPPORTED_CAPABILITIES: Readonly<Record<string, true>> = {
|
|
|
48
48
|
"prompt.command": true,
|
|
49
49
|
"prompt.image": true,
|
|
50
50
|
"prompt.steer": true,
|
|
51
|
+
"session.can_stop_runtime": true,
|
|
51
52
|
"session.configure": true,
|
|
52
53
|
"session.list": true,
|
|
53
54
|
"session.persistence": true,
|
|
@@ -65,6 +66,7 @@ const SUPPORTED_INPUTS: Readonly<Record<string, true>> = {
|
|
|
65
66
|
"session.permission": true,
|
|
66
67
|
"session.revert": true,
|
|
67
68
|
"session.interrupt": true,
|
|
69
|
+
"session.can_stop_runtime": true,
|
|
68
70
|
"session.close": true,
|
|
69
71
|
};
|
|
70
72
|
const MAX_CONNECTION_SESSIONS = 32;
|
|
@@ -1135,6 +1137,21 @@ export function createOmpConnection(
|
|
|
1135
1137
|
await session.interrupt(input);
|
|
1136
1138
|
return;
|
|
1137
1139
|
}
|
|
1140
|
+
case "session.can_stop_runtime": {
|
|
1141
|
+
const session = sessions.get(input.sessionId)?.session;
|
|
1142
|
+
if (!session) {
|
|
1143
|
+
requestFailure(input.requestId, new OmpPublicError("Unknown OMP session"));
|
|
1144
|
+
return;
|
|
1145
|
+
}
|
|
1146
|
+
const canStop = await session.canStopRuntime();
|
|
1147
|
+
emit({
|
|
1148
|
+
type: "session.can_stop_runtime",
|
|
1149
|
+
requestId: input.requestId,
|
|
1150
|
+
sessionId: input.sessionId,
|
|
1151
|
+
canStop,
|
|
1152
|
+
});
|
|
1153
|
+
return;
|
|
1154
|
+
}
|
|
1138
1155
|
case "session.close": {
|
|
1139
1156
|
const slot = sessions.get(input.sessionId);
|
|
1140
1157
|
if (!slot) {
|
|
@@ -155,7 +155,7 @@ export const OMP_RPC_COMMAND_POLICIES = {
|
|
|
155
155
|
predict_word_feedback: "unsupported",
|
|
156
156
|
promote_queued_message: "unsupported",
|
|
157
157
|
remove_queued_message: "unsupported",
|
|
158
|
-
set_ask_dialog: "
|
|
158
|
+
set_ask_dialog: "implemented",
|
|
159
159
|
set_cache_warming: "unsupported",
|
|
160
160
|
set_slow_mode: "unsupported",
|
|
161
161
|
steer_subagent: "unsupported",
|
|
@@ -524,6 +524,10 @@ const OMP_PROVIDER_NAME = NAME.refine((provider) => !provider.includes("/"));
|
|
|
524
524
|
const TEXT = boundedString(MAX_TEXT_LENGTH);
|
|
525
525
|
const RAW_DISPLAY_TEXT = boundedString(MAX_IMAGE_DATA_LENGTH);
|
|
526
526
|
const DISPLAY_TRUNCATION_MARKER = "<truncated>";
|
|
527
|
+
/** OMP's `ask` tool has no upstream caps; these bound one dialog like the select schema does. */
|
|
528
|
+
export const MAX_ASK_QUESTIONS = 32;
|
|
529
|
+
export const MAX_ASK_OPTIONS = 128;
|
|
530
|
+
const MAX_ASK_QUESTION_ID_BYTES = 4_096;
|
|
527
531
|
|
|
528
532
|
function boundRawDisplayContent(value: unknown): unknown {
|
|
529
533
|
if (typeof value === "string") {
|
|
@@ -634,6 +638,7 @@ export const OMP_RPC_DIAGNOSTIC_COMMANDS = [
|
|
|
634
638
|
"get_messages",
|
|
635
639
|
"abort",
|
|
636
640
|
"set_host_tools",
|
|
641
|
+
"set_ask_dialog",
|
|
637
642
|
] as const;
|
|
638
643
|
export type OmpRpcDiagnosticCommand = (typeof OMP_RPC_DIAGNOSTIC_COMMANDS)[number];
|
|
639
644
|
type JsonBoundViolation = {
|
|
@@ -1226,6 +1231,8 @@ export const OmpSessionStateSchema = z.object({
|
|
|
1226
1231
|
thinkingLevel: OmpCurrentThinkingLevelSchema,
|
|
1227
1232
|
isStreaming: z.boolean(),
|
|
1228
1233
|
isCompacting: z.boolean(),
|
|
1234
|
+
hasPendingAsyncWork: z.boolean().optional(),
|
|
1235
|
+
isSettled: z.boolean().optional(),
|
|
1229
1236
|
sessionId: IDENTIFIER,
|
|
1230
1237
|
autoCompactionEnabled: z.boolean().optional(),
|
|
1231
1238
|
contextUsage: OmpContextUsageSchema.nullable().optional().catch(undefined),
|
|
@@ -1695,6 +1702,44 @@ const OmpExtensionUiRequestSchema = z.discriminatedUnion("method", [
|
|
|
1695
1702
|
timeout: z.number().nonnegative().finite().optional(),
|
|
1696
1703
|
})
|
|
1697
1704
|
.strict(),
|
|
1705
|
+
z
|
|
1706
|
+
.object({
|
|
1707
|
+
...ExtensionUiBase,
|
|
1708
|
+
method: z.literal("ask"),
|
|
1709
|
+
questions: z
|
|
1710
|
+
.array(
|
|
1711
|
+
z
|
|
1712
|
+
.object({
|
|
1713
|
+
id: boundedString(MAX_ASK_QUESTION_ID_BYTES, 1),
|
|
1714
|
+
question: boundedString(64 * 1024),
|
|
1715
|
+
header: boundedString(4_096).optional(),
|
|
1716
|
+
options: z
|
|
1717
|
+
.array(
|
|
1718
|
+
z
|
|
1719
|
+
.object({
|
|
1720
|
+
label: boundedString(4_096),
|
|
1721
|
+
description: boundedString(16_384).optional(),
|
|
1722
|
+
preview: boundedString(64 * 1024).optional(),
|
|
1723
|
+
})
|
|
1724
|
+
.strict(),
|
|
1725
|
+
)
|
|
1726
|
+
.max(MAX_ASK_OPTIONS),
|
|
1727
|
+
multi: z.boolean().optional(),
|
|
1728
|
+
recommended: z.number().int().nonnegative().optional(),
|
|
1729
|
+
})
|
|
1730
|
+
.strict(),
|
|
1731
|
+
)
|
|
1732
|
+
.min(1)
|
|
1733
|
+
.max(MAX_ASK_QUESTIONS),
|
|
1734
|
+
timeout: z.number().nonnegative().finite().optional(),
|
|
1735
|
+
})
|
|
1736
|
+
.strict()
|
|
1737
|
+
.superRefine((request, context) => {
|
|
1738
|
+
const ids = new Set(request.questions.map((question) => question.id));
|
|
1739
|
+
if (ids.size !== request.questions.length) {
|
|
1740
|
+
context.addIssue({ code: "custom", message: "invalid extension UI request" });
|
|
1741
|
+
}
|
|
1742
|
+
}),
|
|
1698
1743
|
z.object({ ...ExtensionUiBase, method: z.literal("cancel"), targetId: IDENTIFIER }).strict(),
|
|
1699
1744
|
z
|
|
1700
1745
|
.object({
|
|
@@ -1859,6 +1904,8 @@ export function projectSessionStateResponseData(value: unknown): unknown {
|
|
|
1859
1904
|
"thinkingLevel",
|
|
1860
1905
|
"isStreaming",
|
|
1861
1906
|
"isCompacting",
|
|
1907
|
+
"hasPendingAsyncWork",
|
|
1908
|
+
"isSettled",
|
|
1862
1909
|
"sessionId",
|
|
1863
1910
|
"autoCompactionEnabled",
|
|
1864
1911
|
"contextUsage",
|
|
@@ -1980,7 +2027,37 @@ export interface OmpPersistedSessionMessages {
|
|
|
1980
2027
|
|
|
1981
2028
|
export type OmpAvailableCommand = z.infer<typeof OmpAvailableCommandSchema>;
|
|
1982
2029
|
export type OmpImage = { type: "image"; data: string; mimeType: string };
|
|
1983
|
-
export
|
|
1984
|
-
|
|
1985
|
-
|
|
1986
|
-
|
|
2030
|
+
export const OmpExtensionUiResponseSchema = z.union([
|
|
2031
|
+
z.object({ type: z.literal("extension_ui_response"), id: IDENTIFIER, value: TEXT }).strict(),
|
|
2032
|
+
z
|
|
2033
|
+
.object({ type: z.literal("extension_ui_response"), id: IDENTIFIER, confirmed: z.boolean() })
|
|
2034
|
+
.strict(),
|
|
2035
|
+
z
|
|
2036
|
+
.object({
|
|
2037
|
+
type: z.literal("extension_ui_response"),
|
|
2038
|
+
id: IDENTIFIER,
|
|
2039
|
+
cancelled: z.literal(true),
|
|
2040
|
+
timedOut: z.boolean().optional(),
|
|
2041
|
+
})
|
|
2042
|
+
.strict(),
|
|
2043
|
+
z
|
|
2044
|
+
.object({
|
|
2045
|
+
type: z.literal("extension_ui_response"),
|
|
2046
|
+
id: IDENTIFIER,
|
|
2047
|
+
answers: z
|
|
2048
|
+
.array(
|
|
2049
|
+
z
|
|
2050
|
+
.object({
|
|
2051
|
+
id: boundedString(MAX_ASK_QUESTION_ID_BYTES, 1),
|
|
2052
|
+
selectedOptions: z.array(boundedString(4_096)).max(MAX_ASK_OPTIONS),
|
|
2053
|
+
customInput: TEXT.optional(),
|
|
2054
|
+
})
|
|
2055
|
+
.strict(),
|
|
2056
|
+
)
|
|
2057
|
+
.min(1)
|
|
2058
|
+
.max(MAX_ASK_QUESTIONS),
|
|
2059
|
+
})
|
|
2060
|
+
.strict(),
|
|
2061
|
+
]);
|
|
2062
|
+
export type OmpExtensionUiResponse = z.infer<typeof OmpExtensionUiResponseSchema>;
|
|
2063
|
+
export const OmpAskDialogEnabledSchema = z.object({ enabled: z.boolean() }).strict();
|
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
MAX_REPLAY_MESSAGES,
|
|
11
11
|
NAME,
|
|
12
12
|
OMP_MESSAGE_PAGE_LIMIT,
|
|
13
|
+
OmpAskDialogEnabledSchema,
|
|
13
14
|
type OmpAvailableCommand,
|
|
14
15
|
OmpAvailableCommandsResultSchema,
|
|
15
16
|
OmpBranchMessagesResultSchema,
|
|
@@ -18,6 +19,7 @@ import {
|
|
|
18
19
|
type OmpCompactionResult,
|
|
19
20
|
OmpCompactionResultSchema,
|
|
20
21
|
type OmpExtensionUiResponse,
|
|
22
|
+
OmpExtensionUiResponseSchema,
|
|
21
23
|
type OmpHostToolDefinition,
|
|
22
24
|
OmpHostToolDefinitionSchema,
|
|
23
25
|
type OmpHostToolResult,
|
|
@@ -546,7 +548,10 @@ class OmpRpcSession implements OmpRuntimeSession {
|
|
|
546
548
|
}
|
|
547
549
|
|
|
548
550
|
respondToExtensionUi(response: OmpExtensionUiResponse): Promise<void> {
|
|
549
|
-
|
|
551
|
+
// Callers chain `.catch` without a surrounding try, so a bad frame must reject, not throw.
|
|
552
|
+
const parsed = OmpExtensionUiResponseSchema.safeParse(response);
|
|
553
|
+
if (!parsed.success) return Promise.reject(parsed.error);
|
|
554
|
+
return this.process.sendFrame(parsed.data);
|
|
550
555
|
}
|
|
551
556
|
respondToToolApproval(response: OmpToolApprovalResponse): Promise<void> {
|
|
552
557
|
return this.process.sendFrame(OmpToolApprovalResponseSchema.parse(response));
|
|
@@ -652,6 +657,7 @@ export class OmpRpcRuntime implements OmpRuntime {
|
|
|
652
657
|
}),
|
|
653
658
|
);
|
|
654
659
|
options.signal?.throwIfAborted();
|
|
660
|
+
enableAskDialog(process);
|
|
655
661
|
return new OmpRpcSession(
|
|
656
662
|
process,
|
|
657
663
|
removeAbortListener,
|
|
@@ -671,3 +677,17 @@ export class OmpRpcRuntime implements OmpRuntime {
|
|
|
671
677
|
}
|
|
672
678
|
}
|
|
673
679
|
}
|
|
680
|
+
|
|
681
|
+
/**
|
|
682
|
+
* Opts into OMP's single-request `ask` dialog so every question reaches Paseo as one card that can
|
|
683
|
+
* be revisited before submitting. OMP applies the flag in command order, so it is set before any
|
|
684
|
+
* later prompt can ask; startup does not wait for the acknowledgement. OMP releases without
|
|
685
|
+
* `set_ask_dialog` reject it and keep the per-question select/editor fallback, which the
|
|
686
|
+
* permission bridge still handles.
|
|
687
|
+
*/
|
|
688
|
+
function enableAskDialog(process: OmpRpcProcess): void {
|
|
689
|
+
process
|
|
690
|
+
.request({ type: "set_ask_dialog", enabled: true })
|
|
691
|
+
.then((data) => OmpAskDialogEnabledSchema.parse(data))
|
|
692
|
+
.catch(() => undefined);
|
|
693
|
+
}
|
|
@@ -301,6 +301,10 @@ export class OmpHistorySession implements OmpOpenedSession {
|
|
|
301
301
|
return Promise.resolve();
|
|
302
302
|
}
|
|
303
303
|
|
|
304
|
+
canStopRuntime(): Promise<boolean> {
|
|
305
|
+
return Promise.resolve(true);
|
|
306
|
+
}
|
|
307
|
+
|
|
304
308
|
beginConnectionShutdown(): void {
|
|
305
309
|
this.lifetime.abort(new Error("OMP provider connection closed"));
|
|
306
310
|
}
|
|
@@ -25,14 +25,23 @@ import type { OmpTimelineProjector, OmpTimelineScheduler } from "./timeline-proj
|
|
|
25
25
|
type SessionPermissionInput = Extract<ProviderInput, { type: "session.permission" }>;
|
|
26
26
|
type OmpQuestionRequest = Extract<
|
|
27
27
|
Extract<OmpRpcEvent, { type: "extension_ui_request" }>,
|
|
28
|
-
{ method: "select" | "confirm" | "input" | "editor" }
|
|
28
|
+
{ method: "select" | "confirm" | "input" | "editor" | "ask" }
|
|
29
29
|
>;
|
|
30
|
+
type OmpAskRequest = Extract<OmpQuestionRequest, { method: "ask" }>;
|
|
30
31
|
type Emit = (event: ProviderEvent) => void;
|
|
31
32
|
|
|
32
33
|
const MAX_PENDING_PERMISSIONS = 32;
|
|
33
34
|
const MAX_PENDING_PERMISSION_BYTES = 2 * 1024 * 1024;
|
|
34
35
|
const MAX_RESOLVED_TOOL_APPROVAL_IDS = 1_024;
|
|
35
|
-
|
|
36
|
+
/**
|
|
37
|
+
* Select rows that hand off to a follow-up free-text request: `ask_user` extensions follow with
|
|
38
|
+
* `input`, OMP's own `ask` fallback (hosts without `set_ask_dialog`) follows with `editor`.
|
|
39
|
+
*/
|
|
40
|
+
const OMP_FREEFORM_SENTINELS: readonly string[] = [
|
|
41
|
+
"✏️ Type custom response...",
|
|
42
|
+
"Other (type your own)",
|
|
43
|
+
];
|
|
44
|
+
const OMP_ASK_RECOMMENDED_SUFFIX = " (Recommended)";
|
|
36
45
|
const MAX_FREEFORM_RESPONSE_BYTES = 64 * 1024;
|
|
37
46
|
|
|
38
47
|
type PendingPermission = {
|
|
@@ -50,6 +59,14 @@ type PendingPermission = {
|
|
|
50
59
|
turnId?: string;
|
|
51
60
|
request: OmpQuestionRequest;
|
|
52
61
|
freeformSentinel?: string;
|
|
62
|
+
askQuestions?: readonly PendingAskQuestion[];
|
|
63
|
+
};
|
|
64
|
+
/** One `ask` question as rendered on the Paseo card, with display labels mapped back to OMP. */
|
|
65
|
+
type PendingAskQuestion = {
|
|
66
|
+
id: string;
|
|
67
|
+
header: string;
|
|
68
|
+
multi: boolean;
|
|
69
|
+
nativeLabels: ReadonlyMap<string, string>;
|
|
53
70
|
};
|
|
54
71
|
type PendingToolPermission = {
|
|
55
72
|
nativeId: string;
|
|
@@ -75,6 +92,96 @@ function permissionFingerprint(request: OmpQuestionRequest): string {
|
|
|
75
92
|
return createHash("sha256").update(JSON.stringify(request)).digest("base64url");
|
|
76
93
|
}
|
|
77
94
|
|
|
95
|
+
type CardQuestion = {
|
|
96
|
+
header: string;
|
|
97
|
+
question: string;
|
|
98
|
+
options: Array<{ label: string; value?: string; description?: string; preview?: string }>;
|
|
99
|
+
multiSelect: boolean;
|
|
100
|
+
allowOther?: boolean;
|
|
101
|
+
placeholder?: string;
|
|
102
|
+
prefill?: string;
|
|
103
|
+
};
|
|
104
|
+
type AskAnswer = { id: string; selectedOptions: string[]; customInput?: string };
|
|
105
|
+
|
|
106
|
+
function uniqueLabel(base: string, used: Set<string>): string {
|
|
107
|
+
let label = base;
|
|
108
|
+
let suffix = 2;
|
|
109
|
+
while (used.has(label)) {
|
|
110
|
+
label = `${base} (${suffix})`;
|
|
111
|
+
suffix += 1;
|
|
112
|
+
}
|
|
113
|
+
used.add(label);
|
|
114
|
+
return label;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Paseo's question card submits a multi-select answer as one string: checked option labels in
|
|
119
|
+
* click order, then any typed answer, joined with ", ". Exact labels are consumed from the front,
|
|
120
|
+
* longest first so a label containing ", " wins over its prefix; the rest is the typed answer.
|
|
121
|
+
*/
|
|
122
|
+
function splitMultiSelectAnswer(
|
|
123
|
+
answer: string,
|
|
124
|
+
labels: readonly string[],
|
|
125
|
+
): { selected: string[]; custom: string } {
|
|
126
|
+
const candidates = [...labels].sort((left, right) => right.length - left.length);
|
|
127
|
+
const selected: string[] = [];
|
|
128
|
+
let remaining = answer;
|
|
129
|
+
while (remaining.length > 0) {
|
|
130
|
+
const label = candidates.find(
|
|
131
|
+
(candidate) =>
|
|
132
|
+
!selected.includes(candidate) &&
|
|
133
|
+
(remaining === candidate || remaining.startsWith(`${candidate}, `)),
|
|
134
|
+
);
|
|
135
|
+
if (label === undefined) break;
|
|
136
|
+
selected.push(label);
|
|
137
|
+
remaining = remaining === label ? "" : remaining.slice(label.length + 2);
|
|
138
|
+
}
|
|
139
|
+
return { selected, custom: remaining };
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Converts one card answer to OMP's shape. OMP rejects unknown or repeated labels and, for
|
|
144
|
+
* single-select questions, a selection combined with typed text, so neither is ever produced.
|
|
145
|
+
*/
|
|
146
|
+
function askAnswer(question: PendingAskQuestion, answer: unknown): AskAnswer {
|
|
147
|
+
const parts =
|
|
148
|
+
answer === undefined || answer === null
|
|
149
|
+
? []
|
|
150
|
+
: typeof answer === "string"
|
|
151
|
+
? [answer]
|
|
152
|
+
: Array.isArray(answer) && answer.every((part) => typeof part === "string")
|
|
153
|
+
? answer
|
|
154
|
+
: undefined;
|
|
155
|
+
if (!parts) throw new OmpPublicError("OMP question response is invalid");
|
|
156
|
+
const labels = [...question.nativeLabels.keys()];
|
|
157
|
+
const selectedOptions: string[] = [];
|
|
158
|
+
const custom: string[] = [];
|
|
159
|
+
const select = (label: string) => {
|
|
160
|
+
const native = question.nativeLabels.get(label);
|
|
161
|
+
if (native !== undefined && !selectedOptions.includes(native)) selectedOptions.push(native);
|
|
162
|
+
};
|
|
163
|
+
for (const part of parts) {
|
|
164
|
+
if (question.nativeLabels.has(part)) {
|
|
165
|
+
select(part);
|
|
166
|
+
} else if (question.multi) {
|
|
167
|
+
const split = splitMultiSelectAnswer(part, labels);
|
|
168
|
+
for (const label of split.selected) select(label);
|
|
169
|
+
if (split.custom.trim()) custom.push(split.custom);
|
|
170
|
+
} else if (part.trim()) {
|
|
171
|
+
custom.push(part);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
const customInput = custom.join(", ").trim();
|
|
175
|
+
if (
|
|
176
|
+
customInput.includes("\0") ||
|
|
177
|
+
utf8Bytes(customInput) > MAX_FREEFORM_RESPONSE_BYTES ||
|
|
178
|
+
(!question.multi && selectedOptions.length + (customInput ? 1 : 0) > 1)
|
|
179
|
+
) {
|
|
180
|
+
throw new OmpPublicError("OMP question response is invalid");
|
|
181
|
+
}
|
|
182
|
+
return { id: question.id, selectedOptions, ...(customInput ? { customInput } : {}) };
|
|
183
|
+
}
|
|
184
|
+
|
|
78
185
|
interface PermissionContext {
|
|
79
186
|
runtime: OmpRuntimeSession;
|
|
80
187
|
generation: number;
|
|
@@ -279,7 +386,7 @@ export class OmpSessionPermissions {
|
|
|
279
386
|
}
|
|
280
387
|
|
|
281
388
|
submitPendingFreeformSelection(
|
|
282
|
-
request: Extract<OmpQuestionRequest, { method: "input" }>,
|
|
389
|
+
request: Extract<OmpQuestionRequest, { method: "input" | "editor" }>,
|
|
283
390
|
): boolean {
|
|
284
391
|
const pending = this.pendingFreeformSelection;
|
|
285
392
|
if (!pending) return false;
|
|
@@ -505,10 +612,17 @@ export class OmpSessionPermissions {
|
|
|
505
612
|
if (!existingId) this.permissionSequence += 1;
|
|
506
613
|
const id =
|
|
507
614
|
existingId ?? `omp:permission:${this.permissionNamespace}:${this.permissionSequence}`;
|
|
508
|
-
const header = this.dataFilter.text(
|
|
615
|
+
const header = this.dataFilter.text(
|
|
616
|
+
request.method === "ask"
|
|
617
|
+
? request.questions.length === 1
|
|
618
|
+
? "OMP question"
|
|
619
|
+
: `OMP questions (${request.questions.length})`
|
|
620
|
+
: (request.title ?? "OMP question"),
|
|
621
|
+
4_096,
|
|
622
|
+
);
|
|
509
623
|
const freeformSentinel =
|
|
510
|
-
request.method === "select"
|
|
511
|
-
?
|
|
624
|
+
request.method === "select"
|
|
625
|
+
? request.options.find((option) => OMP_FREEFORM_SENTINELS.includes(option))
|
|
512
626
|
: undefined;
|
|
513
627
|
const optionValues = new Map<string, string>();
|
|
514
628
|
const displayValues = new Map<string, string>();
|
|
@@ -523,14 +637,7 @@ export class OmpSessionPermissions {
|
|
|
523
637
|
const options =
|
|
524
638
|
request.method === "select"
|
|
525
639
|
? selectableOptions.map(({ nativeValue, index }) => {
|
|
526
|
-
const
|
|
527
|
-
let label = baseLabel;
|
|
528
|
-
let suffix = 2;
|
|
529
|
-
while (usedOptionLabels.has(label)) {
|
|
530
|
-
label = `${baseLabel} (${suffix})`;
|
|
531
|
-
suffix += 1;
|
|
532
|
-
}
|
|
533
|
-
usedOptionLabels.add(label);
|
|
640
|
+
const label = uniqueLabel(this.dataFilter.text(nativeValue, 4_096), usedOptionLabels);
|
|
534
641
|
const value = `${id}:option:${index}`;
|
|
535
642
|
optionValues.set(value, nativeValue);
|
|
536
643
|
displayValues.set(label, nativeValue);
|
|
@@ -550,6 +657,28 @@ export class OmpSessionPermissions {
|
|
|
550
657
|
: undefined;
|
|
551
658
|
const questionOptions =
|
|
552
659
|
request.method === "confirm" ? [{ label: "Yes" }, { label: "No" }] : (options ?? []);
|
|
660
|
+
const ask = request.method === "ask" ? this.askCard(request) : undefined;
|
|
661
|
+
const cardQuestions: CardQuestion[] =
|
|
662
|
+
request.method === "ask"
|
|
663
|
+
? (ask?.questions ?? [])
|
|
664
|
+
: [
|
|
665
|
+
{
|
|
666
|
+
header,
|
|
667
|
+
question: this.dataFilter.text(
|
|
668
|
+
request.method === "confirm" ? request.message : request.title,
|
|
669
|
+
64 * 1024,
|
|
670
|
+
),
|
|
671
|
+
options: questionOptions,
|
|
672
|
+
multiSelect: false,
|
|
673
|
+
...(freeformSentinel ? { allowOther: true } : {}),
|
|
674
|
+
...(request.method === "input" && request.placeholder
|
|
675
|
+
? { placeholder: this.dataFilter.text(request.placeholder, 4_096) }
|
|
676
|
+
: {}),
|
|
677
|
+
...((request.method === "input" || request.method === "editor") && request.prefill
|
|
678
|
+
? { prefill: this.dataFilter.text(request.prefill) }
|
|
679
|
+
: {}),
|
|
680
|
+
},
|
|
681
|
+
];
|
|
553
682
|
const actions =
|
|
554
683
|
request.method === "select"
|
|
555
684
|
? [
|
|
@@ -591,7 +720,7 @@ export class OmpSessionPermissions {
|
|
|
591
720
|
runtime: this.runtime,
|
|
592
721
|
request,
|
|
593
722
|
retainedBytes: boundedJsonBytes(
|
|
594
|
-
{ request, header,
|
|
723
|
+
{ request, header, questions: cardQuestions, actions },
|
|
595
724
|
MAX_PENDING_PERMISSION_BYTES,
|
|
596
725
|
512,
|
|
597
726
|
MAX_PENDING_PERMISSION_BYTES,
|
|
@@ -600,6 +729,7 @@ export class OmpSessionPermissions {
|
|
|
600
729
|
...(this.activeTurn ? { turnId: this.activeTurn.turnId } : {}),
|
|
601
730
|
...(request.timeout !== undefined ? { expiresAt: Date.now() + request.timeout } : {}),
|
|
602
731
|
...(freeformSentinel ? { freeformSentinel } : {}),
|
|
732
|
+
...(ask ? { askQuestions: ask.pending } : {}),
|
|
603
733
|
};
|
|
604
734
|
let retainedPermissionBytes = pending.retainedBytes;
|
|
605
735
|
for (const [permissionId, retained] of this.pendingPermissions) {
|
|
@@ -647,26 +777,7 @@ export class OmpSessionPermissions {
|
|
|
647
777
|
...(request.method === "confirm"
|
|
648
778
|
? { description: this.dataFilter.text(request.message, 64 * 1024) }
|
|
649
779
|
: {}),
|
|
650
|
-
input: {
|
|
651
|
-
questions: [
|
|
652
|
-
{
|
|
653
|
-
header,
|
|
654
|
-
question: this.dataFilter.text(
|
|
655
|
-
request.method === "confirm" ? request.message : request.title,
|
|
656
|
-
64 * 1024,
|
|
657
|
-
),
|
|
658
|
-
options: questionOptions,
|
|
659
|
-
multiSelect: false,
|
|
660
|
-
...(freeformSentinel ? { allowOther: true } : {}),
|
|
661
|
-
...(request.method === "input" && request.placeholder
|
|
662
|
-
? { placeholder: this.dataFilter.text(request.placeholder, 4_096) }
|
|
663
|
-
: {}),
|
|
664
|
-
...((request.method === "input" || request.method === "editor") && request.prefill
|
|
665
|
-
? { prefill: this.dataFilter.text(request.prefill) }
|
|
666
|
-
: {}),
|
|
667
|
-
},
|
|
668
|
-
],
|
|
669
|
-
},
|
|
780
|
+
input: { questions: cardQuestions },
|
|
670
781
|
actions,
|
|
671
782
|
},
|
|
672
783
|
});
|
|
@@ -688,6 +799,76 @@ export class OmpSessionPermissions {
|
|
|
688
799
|
.catch(() => this.handleRuntimeFailure());
|
|
689
800
|
}
|
|
690
801
|
|
|
802
|
+
/**
|
|
803
|
+
* Renders every `ask` question on one card. Headers key the card's answers and option labels
|
|
804
|
+
* come back verbatim, so both are made unique here and mapped back to OMP's labels on submit.
|
|
805
|
+
*/
|
|
806
|
+
private askCard(request: OmpAskRequest): {
|
|
807
|
+
questions: CardQuestion[];
|
|
808
|
+
pending: PendingAskQuestion[];
|
|
809
|
+
} {
|
|
810
|
+
const usedHeaders = new Set<string>();
|
|
811
|
+
const questions: CardQuestion[] = [];
|
|
812
|
+
const pending: PendingAskQuestion[] = [];
|
|
813
|
+
for (const [index, question] of request.questions.entries()) {
|
|
814
|
+
const header = uniqueLabel(
|
|
815
|
+
this.dataFilter.text(question.header?.trim() || `Question ${index + 1}`, 4_096),
|
|
816
|
+
usedHeaders,
|
|
817
|
+
);
|
|
818
|
+
const usedLabels = new Set<string>();
|
|
819
|
+
const nativeLabels = new Map<string, string>();
|
|
820
|
+
const options = question.options.map((option, optionIndex) => {
|
|
821
|
+
const label = uniqueLabel(
|
|
822
|
+
`${this.dataFilter.text(option.label, 4_096)}${
|
|
823
|
+
optionIndex === question.recommended ? OMP_ASK_RECOMMENDED_SUFFIX : ""
|
|
824
|
+
}`,
|
|
825
|
+
usedLabels,
|
|
826
|
+
);
|
|
827
|
+
nativeLabels.set(label, option.label);
|
|
828
|
+
return {
|
|
829
|
+
label,
|
|
830
|
+
...(option.description
|
|
831
|
+
? { description: this.dataFilter.text(option.description, 16_384) }
|
|
832
|
+
: {}),
|
|
833
|
+
...(option.preview ? { preview: this.dataFilter.text(option.preview, 64 * 1024) } : {}),
|
|
834
|
+
};
|
|
835
|
+
});
|
|
836
|
+
const multi = question.multi === true;
|
|
837
|
+
questions.push({
|
|
838
|
+
header,
|
|
839
|
+
question: this.dataFilter.text(question.question, 64 * 1024),
|
|
840
|
+
options,
|
|
841
|
+
multiSelect: multi,
|
|
842
|
+
allowOther: true,
|
|
843
|
+
});
|
|
844
|
+
pending.push({ id: question.id, header, multi, nativeLabels });
|
|
845
|
+
}
|
|
846
|
+
return { questions, pending };
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
/** Maps card answers to OMP's per-question `selectedOptions`/`customInput` in question order. */
|
|
850
|
+
private askResponse(
|
|
851
|
+
pending: PendingPermission,
|
|
852
|
+
response: ProviderPermissionResponse,
|
|
853
|
+
): OmpExtensionUiResponse {
|
|
854
|
+
const answers = response.behavior === "allow" ? response.updatedInput?.answers : undefined;
|
|
855
|
+
if (
|
|
856
|
+
!pending.askQuestions ||
|
|
857
|
+
!answers ||
|
|
858
|
+
typeof answers !== "object" ||
|
|
859
|
+
Array.isArray(answers)
|
|
860
|
+
) {
|
|
861
|
+
throw new OmpPublicError("OMP question response requires answers");
|
|
862
|
+
}
|
|
863
|
+
return {
|
|
864
|
+
type: "extension_ui_response",
|
|
865
|
+
id: pending.nativeId,
|
|
866
|
+
answers: pending.askQuestions.map((question) =>
|
|
867
|
+
askAnswer(question, (answers as Record<string, unknown>)[question.header]),
|
|
868
|
+
),
|
|
869
|
+
};
|
|
870
|
+
}
|
|
871
|
+
|
|
691
872
|
private freeformSelection(
|
|
692
873
|
pending: PendingPermission,
|
|
693
874
|
response: ProviderPermissionResponse,
|
|
@@ -754,6 +935,7 @@ export class OmpSessionPermissions {
|
|
|
754
935
|
if (response.behavior === "deny") {
|
|
755
936
|
return { type: "extension_ui_response", id: nativeId, cancelled: true };
|
|
756
937
|
}
|
|
938
|
+
if (request.method === "ask") return this.askResponse(pending, response);
|
|
757
939
|
const selectedValue = response.selectedActionId
|
|
758
940
|
? pending.optionValues.get(response.selectedActionId)
|
|
759
941
|
: undefined;
|
|
@@ -968,6 +1150,16 @@ export class OmpSessionPermissions {
|
|
|
968
1150
|
return this.activeTurn?.turnId === pending.turnId && !this.activeTurn.terminal;
|
|
969
1151
|
}
|
|
970
1152
|
|
|
1153
|
+
hasPending(): boolean {
|
|
1154
|
+
return (
|
|
1155
|
+
this.pendingPermissions.size > 0 ||
|
|
1156
|
+
this.inFlightPermissions.size > 0 ||
|
|
1157
|
+
this.pendingToolPermissions.size > 0 ||
|
|
1158
|
+
this.inFlightToolPermissions.size > 0 ||
|
|
1159
|
+
this.pendingFreeformSelection !== null
|
|
1160
|
+
);
|
|
1161
|
+
}
|
|
1162
|
+
|
|
971
1163
|
hasForTurn(turnId: string): boolean {
|
|
972
1164
|
const ownsTurn = (pending: PendingPermission | PendingToolPermission) =>
|
|
973
1165
|
pending.turnId === turnId;
|
|
@@ -111,6 +111,7 @@ export interface OmpOpenedSession {
|
|
|
111
111
|
configure(input: SessionConfigureInput): Promise<void>;
|
|
112
112
|
revert(input: SessionRevertInput): Promise<void>;
|
|
113
113
|
interrupt(input: SessionInterruptInput): Promise<void>;
|
|
114
|
+
canStopRuntime(): Promise<boolean>;
|
|
114
115
|
close(input?: SessionCloseInput): Promise<void>;
|
|
115
116
|
abortOpen(): Promise<void>;
|
|
116
117
|
beginConnectionShutdown(): void;
|
|
@@ -1465,6 +1466,28 @@ export class OmpProviderSession implements OmpOpenedSession {
|
|
|
1465
1466
|
}
|
|
1466
1467
|
}
|
|
1467
1468
|
|
|
1469
|
+
async canStopRuntime(): Promise<boolean> {
|
|
1470
|
+
const hasLocalWork =
|
|
1471
|
+
this.closed ||
|
|
1472
|
+
this.runtimeDead !== null ||
|
|
1473
|
+
this.recoveryPromise !== null ||
|
|
1474
|
+
this.activeTurn !== null ||
|
|
1475
|
+
this.activeAbort !== null ||
|
|
1476
|
+
this.revertInFlight ||
|
|
1477
|
+
this.configMutationInFlight ||
|
|
1478
|
+
this.permissions.hasPending() ||
|
|
1479
|
+
Boolean(this.subsessions?.hasActiveChildren());
|
|
1480
|
+
if (hasLocalWork) return false;
|
|
1481
|
+
const state = await this.runtime.getState();
|
|
1482
|
+
// OMP builds that predate these fields report neither, which keeps the runtime alive.
|
|
1483
|
+
return (
|
|
1484
|
+
state.isSettled === true &&
|
|
1485
|
+
state.hasPendingAsyncWork === false &&
|
|
1486
|
+
!state.isStreaming &&
|
|
1487
|
+
!state.isCompacting
|
|
1488
|
+
);
|
|
1489
|
+
}
|
|
1490
|
+
|
|
1468
1491
|
async interrupt(input: SessionInterruptInput): Promise<void> {
|
|
1469
1492
|
const turn = this.activeTurn;
|
|
1470
1493
|
if (!turn) {
|
|
@@ -2280,13 +2303,17 @@ export class OmpProviderSession implements OmpOpenedSession {
|
|
|
2280
2303
|
this.permissions.resolvePermissionByNativeId(event.targetId ?? "");
|
|
2281
2304
|
return;
|
|
2282
2305
|
}
|
|
2283
|
-
if (
|
|
2306
|
+
if (
|
|
2307
|
+
(event.method === "input" || event.method === "editor") &&
|
|
2308
|
+
this.permissions.submitPendingFreeformSelection(event)
|
|
2309
|
+
)
|
|
2284
2310
|
return;
|
|
2285
2311
|
if (
|
|
2286
2312
|
event.method === "select" ||
|
|
2287
2313
|
event.method === "confirm" ||
|
|
2288
2314
|
event.method === "input" ||
|
|
2289
|
-
event.method === "editor"
|
|
2315
|
+
event.method === "editor" ||
|
|
2316
|
+
event.method === "ask"
|
|
2290
2317
|
) {
|
|
2291
2318
|
if (!this.capabilities.includes("permission")) {
|
|
2292
2319
|
this.handleRuntimeFailure();
|
|
@@ -2459,7 +2486,9 @@ export class OmpProviderSession implements OmpOpenedSession {
|
|
|
2459
2486
|
if (event.type === "tool_execution_start") turn.activeToolCallIds.add(event.toolCallId);
|
|
2460
2487
|
if (event.type === "tool_execution_end") {
|
|
2461
2488
|
turn.activeToolCallIds.delete(event.toolCallId);
|
|
2462
|
-
if (event.toolName === "ask_user"
|
|
2489
|
+
if (event.toolName === "ask_user" || event.toolName === "ask") {
|
|
2490
|
+
this.permissions.clearPendingFreeformSelection();
|
|
2491
|
+
}
|
|
2463
2492
|
}
|
|
2464
2493
|
if (event.type === "tool_execution_start" || event.type === "tool_execution_end") {
|
|
2465
2494
|
try {
|