@oar-sh/oar 0.9.2 → 0.9.4
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/.github/extensions/web-relay/extension.mjs +6 -1
- package/.github/extensions/web-relay/skills/tool-activity.mjs +7 -0
- package/CHANGELOG.md +336 -0
- package/README.md +455 -423
- package/bin/oar.js +25 -10
- package/package.json +11 -5
- package/server/README.md +44 -4
- package/server/claude-worker/claude-remote-relay-tool.mjs +99 -0
- package/server/claude-worker/claude-sdk-adapter.mjs +26 -4
- package/server/claude-worker/claude-session-process.mjs +1073 -69
- package/server/claude-worker/claude-session-worker.mjs +45 -4
- package/server/claude-worker/claude-turn-publisher.mjs +81 -20
- package/server/claude-worker/claude-worker-link-wiring.mjs +9 -0
- package/server/claude-worker/sdk-message-normalizer.mjs +7 -0
- package/server/copilot-worker/copilot-ask-user-tool.mjs +89 -0
- package/server/copilot-worker/copilot-continuation-signals.mjs +55 -0
- package/server/copilot-worker/copilot-question-bridge.mjs +34 -12
- package/server/copilot-worker/copilot-remote-relay-tool.mjs +59 -0
- package/server/copilot-worker/copilot-sdk-event-normalizer.mjs +113 -14
- package/server/copilot-worker/copilot-sdk-session-process.mjs +2349 -498
- package/server/copilot-worker/copilot-sdk-session-worker.mjs +82 -20
- package/server/cursor-worker/cursor-remote-relay-tool.mjs +65 -0
- package/server/cursor-worker/cursor-turn-runner.mjs +45 -2
- package/server/cursor-worker/sdk-message-normalizer.mjs +25 -0
- package/server/db-schema.mjs +28 -0
- package/server/grok-worker/acp-client.mjs +92 -6
- package/server/grok-worker/grok-turn-runner.mjs +59 -8
- package/server/grok-worker/sdk-message-normalizer.mjs +13 -0
- package/server/mcp/oar-mcp-server.mjs +473 -0
- package/server/migrations/0004-steer-settle-markers.mjs +119 -0
- package/server/migrations/0005-remote-relays.mjs +90 -0
- package/server/public/app/action-confirmations.js +141 -13
- package/server/public/app/api-client.js +125 -2
- package/server/public/app/ask-user-view.js +102 -13
- package/server/public/app/attachments-view.js +37 -2
- package/server/public/app/background-task-timeout-options.mjs +9 -0
- package/server/public/app/background-tasks-view.mjs +33 -18
- package/server/public/app/bootstrap.js +55 -14
- package/server/public/app/composer-control-state.mjs +52 -34
- package/server/public/app/conversation-deep-link.mjs +42 -0
- package/server/public/app/conversation-draft-sync.mjs +187 -0
- package/server/public/app/conversation-list-filter.mjs +86 -0
- package/server/public/app/conversation-provider-indicator.mjs +8 -0
- package/server/public/app/conversation-view.js +1016 -200
- package/server/public/app/emoji-view.js +2 -0
- package/server/public/app/font-scaling.js +4 -37
- package/server/public/app/host-suspend-ui.js +272 -0
- package/server/public/app/journal-view.js +149 -6
- package/server/public/app/mention-autocomplete.mjs +298 -0
- package/server/public/app/model-selector-options.mjs +1 -1
- package/server/public/app/push-settings.js +1 -0
- package/server/public/app/pwa-install.js +8 -3
- package/server/public/app/question-multi-select.mjs +52 -0
- package/server/public/app/relay-prompt-sanitizer.mjs +61 -3
- package/server/public/app/remote-relay-origin-view.mjs +76 -0
- package/server/public/app/remote-relay-shared.mjs +85 -0
- package/server/public/app/remote-relays-settings.js +618 -0
- package/server/public/app/remote-relays-store.mjs +126 -0
- package/server/public/app/settings-modal.js +12 -8
- package/server/public/app/settings-tabs.js +1 -1
- package/server/public/app/socket-handlers.js +37 -1
- package/server/public/app/steered-turn-merge.mjs +100 -0
- package/server/public/app/store.js +126 -15
- package/server/public/app/sync-outbox.mjs +16 -8
- package/server/public/app/thinking-follow.js +260 -0
- package/server/public/app/tool-activity-emoji.mjs +40 -0
- package/server/public/app/transcript-viewport-anchor.mjs +210 -0
- package/server/public/index.html +463 -17
- package/server/public/sw.js +7 -0
- package/server/relay-tools.md +7 -1
- package/server/repositories/message-repository.mjs +68 -18
- package/server/repositories/question-repository.mjs +18 -0
- package/server/repositories/remote-relay-repository.mjs +75 -0
- package/server/repositories/session-repository.mjs +13 -32
- package/server/routes/ask-user-routes.mjs +42 -2
- package/server/routes/messages-routes.mjs +572 -56
- package/server/routes/remote-relay-routes.mjs +133 -0
- package/server/routes/sessions-routes.mjs +378 -84
- package/server/server-runtime.mjs +303 -25
- package/server/services/github-ci-activity-service.mjs +199 -0
- package/server/services/host-suspend-command.mjs +38 -0
- package/server/services/host-suspend-service.mjs +254 -0
- package/server/services/live-turn-picker.mjs +47 -0
- package/server/services/push-dispatch-service.mjs +37 -4
- package/server/services/relay-prompt-sanitizer.mjs +17 -2
- package/server/services/relay-question-context.mjs +46 -0
- package/server/services/remote-relay-caller-context.mjs +341 -0
- package/server/services/remote-relay-client.mjs +332 -0
- package/server/services/remote-relay-dispatcher.mjs +1500 -0
- package/server/services/remote-relay-inbound.mjs +187 -0
- package/server/services/remote-relay-pairing.mjs +189 -0
- package/server/services/remote-relay-registry-service.mjs +581 -0
- package/server/services/sdk-session-import-service.mjs +49 -7
- package/server/services/session-worker-launch-service.mjs +131 -12
- package/server/services/session-worker-registry-service.mjs +75 -0
- package/server/services/session-worker-supervisor-service.mjs +7 -1
- package/server/services/session-worker-websocket-service.mjs +23 -1
- package/server/services/steer-resend.mjs +45 -0
- package/shared/background-task-timeout.mjs +12 -4
- package/shared/control-poller.mjs +4 -1
- package/shared/question-multi-select.mjs +16 -0
- package/shared/relay-stop-failure.mjs +15 -0
- package/shared/remote-relay-contract.mjs +539 -0
- package/shared/remote-relay-mentions.mjs +129 -0
- package/shared/remote-relay-tool-core.mjs +314 -0
- package/shared/slash-command-guard.mjs +76 -0
- package/shared/steer-note.mjs +66 -0
- package/shared/steer-settle-failure.mjs +89 -0
- package/shared/steer-settle-markers.mjs +18 -0
- package/shared/worker-crash-guard.mjs +40 -25
- package/shared/worker-runtime/api-client.mjs +112 -22
- package/shared/worker-runtime/heartbeat.mjs +55 -1
- package/shared/worker-runtime/runner-link-wiring.mjs +44 -0
- package/shared/worker-runtime/worker-websocket-link.mjs +119 -8
- package/server/claude-worker/claude-session-test-harness.mjs +0 -250
- package/server/claude-worker/fixtures/workflow-run-record.json +0 -1
- package/server/copilot-worker/copilot-sdk-test-harness.mjs +0 -382
- package/server/copilot-worker/fixtures/abort-turn.json +0 -588
- package/server/copilot-worker/fixtures/ask-user-turn.json +0 -1154
- package/server/copilot-worker/fixtures/background-timer-continuation.json +0 -445
- package/server/copilot-worker/fixtures/background-timer-turn.json +0 -497
- package/server/copilot-worker/fixtures/happy-turn.json +0 -566
- package/server/copilot-worker/fixtures/quota-turn.json +0 -647
- package/server/copilot-worker/fixtures/reasoning-turn.json +0 -625
- package/server/copilot-worker/fixtures/resume-turn.json +0 -700
- package/server/copilot-worker/fixtures/subagent-turn.json +0 -1378
- package/server/copilot-worker/fixtures/tool-permission-turn.json +0 -1293
- package/server/routes/messages-routes-test-harness.mjs +0 -111
- package/server/services/fixtures/claude-auth-stub.cmd +0 -1
- package/server/services/fixtures/claude-auth-stub.mjs +0 -76
- package/server/services/fixtures/claude-auth-stub.sh +0 -4
- package/server/services/fixtures/cli-install-stub.cmd +0 -1
- package/server/services/fixtures/cli-install-stub.mjs +0 -98
- package/server/services/fixtures/cli-install-stub.sh +0 -4
- package/server/services/fixtures/grok-stub.cmd +0 -1
- package/server/services/fixtures/grok-stub.mjs +0 -89
- package/server/services/fixtures/grok-stub.sh +0 -4
- package/shared/fixtures/copilot-catalog-raw-2026-09-07.json +0 -2363
|
@@ -1,382 +0,0 @@
|
|
|
1
|
-
// Spawn-free stand-ins for the Copilot SDK, per DEVELOPING.md: no test in this
|
|
2
|
-
// directory starts a runtime process, imports the installed SDK, or talks to a
|
|
3
|
-
// relay. The injection seam is a constructor option on
|
|
4
|
-
// `createCopilotSdkSessionRunner` (`startClientImpl`), not a module mock.
|
|
5
|
-
//
|
|
6
|
-
// Deliberately NOT named `*.test.mjs` so `node --test`'s glob skips it.
|
|
7
|
-
import fs from 'node:fs';
|
|
8
|
-
import path from 'node:path';
|
|
9
|
-
import { fileURLToPath } from 'node:url';
|
|
10
|
-
|
|
11
|
-
import { createCopilotSdkSessionRunner } from './copilot-sdk-session-process.mjs';
|
|
12
|
-
import { renderMediaEmbedInstructionBlock } from '../../shared/media-embed-instructions.mjs';
|
|
13
|
-
|
|
14
|
-
const FIXTURE_DIR = path.join(path.dirname(fileURLToPath(import.meta.url)), 'fixtures');
|
|
15
|
-
|
|
16
|
-
/** Load a scrubbed phase-0 event dump as an array of `SessionEvent`s. */
|
|
17
|
-
export function loadFixture(name) {
|
|
18
|
-
return JSON.parse(fs.readFileSync(path.join(FIXTURE_DIR, `${name}.json`), 'utf8'));
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
export function tick(ms = 5) {
|
|
22
|
-
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
export async function waitFor(predicate, { timeoutMs = 3000, label = 'condition' } = {}) {
|
|
26
|
-
const deadline = Date.now() + timeoutMs;
|
|
27
|
-
while (Date.now() < deadline) {
|
|
28
|
-
if (await predicate()) return true;
|
|
29
|
-
await tick(2);
|
|
30
|
-
}
|
|
31
|
-
throw new Error(`waitFor timed out: ${label}`);
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
/**
|
|
35
|
-
* Records every relay call; `failRoutes` makes a route throw, like a 500.
|
|
36
|
-
*
|
|
37
|
-
* `routeResponses` maps a route to its response body, or to a function of
|
|
38
|
-
* `(body, callIndexForThatRoute)` — which is how a test drives a route that
|
|
39
|
-
* answers differently per attempt (`/api/continuation-turn`'s retry loop).
|
|
40
|
-
* Unlisted routes answer `{}`, as before.
|
|
41
|
-
*/
|
|
42
|
-
export function makeApiStub({ failRoutes = new Set(), routeResponses = {} } = {}) {
|
|
43
|
-
const calls = [];
|
|
44
|
-
const routeCounts = new Map();
|
|
45
|
-
async function api(method, routePath, body) {
|
|
46
|
-
calls.push({ method, routePath, body });
|
|
47
|
-
const route = String(routePath).split('?')[0];
|
|
48
|
-
if (failRoutes.has(route)) throw new Error(`stub route failure: ${route}`);
|
|
49
|
-
const attempt = routeCounts.get(route) || 0;
|
|
50
|
-
routeCounts.set(route, attempt + 1);
|
|
51
|
-
const canned = routeResponses[route];
|
|
52
|
-
if (typeof canned === 'function') return canned(body, attempt);
|
|
53
|
-
return canned === undefined ? {} : canned;
|
|
54
|
-
}
|
|
55
|
-
api.calls = calls;
|
|
56
|
-
api.bodiesFor = (routePath) => calls.filter((c) => c.routePath === routePath).map((c) => c.body);
|
|
57
|
-
return api;
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
/**
|
|
61
|
-
* An api stub that mints synthetic queue rows, so a test can exercise the
|
|
62
|
-
* continuation path end to end. `POST /api/continuation-turn` answers with a
|
|
63
|
-
* fresh `messageId` and the `attemptId` the row was dequeued under, exactly as
|
|
64
|
-
* the relay route does.
|
|
65
|
-
*/
|
|
66
|
-
export function makeContinuationApiStub({
|
|
67
|
-
conversationId = 'conv-1',
|
|
68
|
-
messageIds = ['cont-1', 'cont-2', 'cont-3'],
|
|
69
|
-
...options
|
|
70
|
-
} = {}) {
|
|
71
|
-
const minted = [];
|
|
72
|
-
const stub = makeApiStub({
|
|
73
|
-
...options,
|
|
74
|
-
routeResponses: {
|
|
75
|
-
'/api/continuation-turn': (body, attempt) => {
|
|
76
|
-
const messageId = messageIds[Math.min(attempt, messageIds.length - 1)];
|
|
77
|
-
const attemptId = `attempt-${messageId}`;
|
|
78
|
-
minted.push({ messageId, attemptId, body });
|
|
79
|
-
return { ok: true, messageId, conversationId, attemptId };
|
|
80
|
-
},
|
|
81
|
-
...(options.routeResponses || {}),
|
|
82
|
-
},
|
|
83
|
-
});
|
|
84
|
-
stub.mintedContinuations = minted;
|
|
85
|
-
return stub;
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
/**
|
|
89
|
-
* A fake CopilotSession. `send()` accepts one `MessageOptions` object (the
|
|
90
|
-
* real signature: there is no second parameter) and, like the real SDK,
|
|
91
|
-
* resolves immediately — the turn's completion arrives on the event callback,
|
|
92
|
-
* which is what `onSend` is for.
|
|
93
|
-
*
|
|
94
|
-
* The `rpc.model` surface mirrors SDK 1.0.13's, which the runner talks to
|
|
95
|
-
* DIRECTLY (the facade's `setModel` discards the switch result). Defaults model
|
|
96
|
-
* a healthy runtime: `switchTo` confirms the requested model immediately,
|
|
97
|
-
* `setReasoningEffort` echoes the level, `list` answers with `modelCatalog`
|
|
98
|
-
* (empty by default — effort validation then degrades to attempting the RPC).
|
|
99
|
-
* `modelRpc.{switchTo,setReasoningEffort,list}` override per test, each called
|
|
100
|
-
* as `(params, session)`.
|
|
101
|
-
*/
|
|
102
|
-
export function createFakeCopilotSession({ config, onSend = null, onAbort = null, modelRpc = {} }) {
|
|
103
|
-
const session = {
|
|
104
|
-
sessionId: config?.sessionId || 'fake-session',
|
|
105
|
-
config,
|
|
106
|
-
sends: [],
|
|
107
|
-
abortCalls: 0,
|
|
108
|
-
setModelCalls: [],
|
|
109
|
-
disconnected: false,
|
|
110
|
-
emit(event) {
|
|
111
|
-
config?.onEvent?.(event);
|
|
112
|
-
},
|
|
113
|
-
replay(events) {
|
|
114
|
-
for (const event of events) session.emit(event);
|
|
115
|
-
},
|
|
116
|
-
async send(options) {
|
|
117
|
-
session.sends.push(options);
|
|
118
|
-
// Scheduled rather than synchronous so the runner's `await session.send`
|
|
119
|
-
// resolves before the events land, matching the real ordering.
|
|
120
|
-
setTimeout(() => { onSend?.(session, options); }, 0);
|
|
121
|
-
return 'fake-message-id';
|
|
122
|
-
},
|
|
123
|
-
async abort() {
|
|
124
|
-
session.abortCalls += 1;
|
|
125
|
-
setTimeout(() => { onAbort?.(session); }, 0);
|
|
126
|
-
},
|
|
127
|
-
async setModel(model) {
|
|
128
|
-
session.setModelCalls.push(model);
|
|
129
|
-
},
|
|
130
|
-
async disconnect() {
|
|
131
|
-
session.disconnected = true;
|
|
132
|
-
},
|
|
133
|
-
};
|
|
134
|
-
session.rpc = {
|
|
135
|
-
model: {
|
|
136
|
-
switchToCalls: [],
|
|
137
|
-
effortCalls: [],
|
|
138
|
-
listCalls: 0,
|
|
139
|
-
async switchTo(params) {
|
|
140
|
-
session.rpc.model.switchToCalls.push(params);
|
|
141
|
-
if (modelRpc.switchTo) return modelRpc.switchTo(params, session);
|
|
142
|
-
return { modelId: params.modelId };
|
|
143
|
-
},
|
|
144
|
-
async setReasoningEffort(params) {
|
|
145
|
-
session.rpc.model.effortCalls.push(params);
|
|
146
|
-
if (modelRpc.setReasoningEffort) return modelRpc.setReasoningEffort(params, session);
|
|
147
|
-
return { reasoningEffort: params.reasoningEffort };
|
|
148
|
-
},
|
|
149
|
-
async list(params) {
|
|
150
|
-
session.rpc.model.listCalls += 1;
|
|
151
|
-
if (modelRpc.list) return modelRpc.list(params, session);
|
|
152
|
-
return { list: Array.isArray(modelRpc.catalog) ? modelRpc.catalog : [] };
|
|
153
|
-
},
|
|
154
|
-
},
|
|
155
|
-
};
|
|
156
|
-
return session;
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
/**
|
|
160
|
-
* The two ways `resumeSession` fails, which the runner MUST tell apart.
|
|
161
|
-
*
|
|
162
|
-
* - `missing` — the runtime has no state under this id. Shaped like the
|
|
163
|
-
* real thing: the runtime throws `Session not found: <id>`
|
|
164
|
-
* and JSON-RPC wraps it as a generic InternalError (-32603),
|
|
165
|
-
* so the message is the only signal.
|
|
166
|
-
* - `transient` — the call itself failed (connection dropped). Falling
|
|
167
|
-
* through to `createSession` here would blank a live
|
|
168
|
-
* conversation.
|
|
169
|
-
*/
|
|
170
|
-
export function makeResumeFailure(flavor, sessionId = 'conv-1') {
|
|
171
|
-
if (flavor === 'transient') {
|
|
172
|
-
const error = new Error('Pending response rejected since connection got disposed');
|
|
173
|
-
error.code = -32097;
|
|
174
|
-
return error;
|
|
175
|
-
}
|
|
176
|
-
const error = new Error(`Request session.resume failed with message: Session not found: ${sessionId}`);
|
|
177
|
-
error.code = -32603;
|
|
178
|
-
return error;
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
/**
|
|
182
|
-
* A fake CopilotClient. `resumeAvailable: false` models a brand-new
|
|
183
|
-
* conversation (the runtime has no state to resume), which is what makes the
|
|
184
|
-
* runner fall through to `createSession`. `resumeFailure: 'transient'` models
|
|
185
|
-
* the RPC itself failing, which must NOT fall through.
|
|
186
|
-
*/
|
|
187
|
-
export function createFakeCopilotClient({
|
|
188
|
-
resumeAvailable = false,
|
|
189
|
-
resumeFailure = 'missing',
|
|
190
|
-
onSend = null,
|
|
191
|
-
onAbort = null,
|
|
192
|
-
// `rpc.model` behaviour for every session this client mints; see
|
|
193
|
-
// `createFakeCopilotSession`.
|
|
194
|
-
modelRpc = {},
|
|
195
|
-
} = {}) {
|
|
196
|
-
const client = {
|
|
197
|
-
stopped: 0,
|
|
198
|
-
resumeAttempts: [],
|
|
199
|
-
createAttempts: [],
|
|
200
|
-
sessions: [],
|
|
201
|
-
resumeAvailable,
|
|
202
|
-
resumeFailure,
|
|
203
|
-
async resumeSession(sessionId, config) {
|
|
204
|
-
client.resumeAttempts.push({ sessionId, config });
|
|
205
|
-
if (!client.resumeAvailable) throw makeResumeFailure(client.resumeFailure, sessionId);
|
|
206
|
-
const session = createFakeCopilotSession({ config, onSend, onAbort, modelRpc });
|
|
207
|
-
session.resumed = true;
|
|
208
|
-
client.sessions.push(session);
|
|
209
|
-
// A session that exists once exists forever, so later reconnects resume.
|
|
210
|
-
client.resumeAvailable = true;
|
|
211
|
-
return session;
|
|
212
|
-
},
|
|
213
|
-
async createSession(config) {
|
|
214
|
-
client.createAttempts.push(config);
|
|
215
|
-
const session = createFakeCopilotSession({ config, onSend, onAbort, modelRpc });
|
|
216
|
-
client.sessions.push(session);
|
|
217
|
-
client.resumeAvailable = true;
|
|
218
|
-
return session;
|
|
219
|
-
},
|
|
220
|
-
async stop() {
|
|
221
|
-
client.stopped += 1;
|
|
222
|
-
return [];
|
|
223
|
-
},
|
|
224
|
-
};
|
|
225
|
-
Object.defineProperty(client, 'session', {
|
|
226
|
-
get: () => client.sessions[client.sessions.length - 1] || null,
|
|
227
|
-
});
|
|
228
|
-
return client;
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
export const FAKE_SDK_PATHS = {
|
|
232
|
-
sdkDir: '/opt/copilot/pkg/linux-x64/1.0.82/copilot-sdk',
|
|
233
|
-
sdkEntry: '/opt/copilot/pkg/linux-x64/1.0.82/copilot-sdk/index.js',
|
|
234
|
-
versionDir: '/opt/copilot/pkg/linux-x64/1.0.82',
|
|
235
|
-
runtimeEntry: '/opt/copilot/pkg/linux-x64/1.0.82/app.js',
|
|
236
|
-
version: '1.0.82',
|
|
237
|
-
};
|
|
238
|
-
|
|
239
|
-
/** Frozen delivery message; tests derive variants by spread. */
|
|
240
|
-
export const baseMessage = Object.freeze({
|
|
241
|
-
id: 'q-1',
|
|
242
|
-
conversationId: 'conv-1',
|
|
243
|
-
relayMode: 'agent',
|
|
244
|
-
text: 'hello',
|
|
245
|
-
model: 'gpt-5-mini',
|
|
246
|
-
});
|
|
247
|
-
|
|
248
|
-
/**
|
|
249
|
-
* A stand-in for the relay question bridge.
|
|
250
|
-
*
|
|
251
|
-
* `answers` maps a matcher to a canned reply, so a test can drive `ask_user`
|
|
252
|
-
* and ask-mode tool approvals without a relay. Every call is recorded.
|
|
253
|
-
*/
|
|
254
|
-
export function makeFakeQuestionBridge({
|
|
255
|
-
userInputAnswer = 'the human answer',
|
|
256
|
-
userInputTimedOut = false,
|
|
257
|
-
approve = true,
|
|
258
|
-
approvalTimedOut = false,
|
|
259
|
-
approvalFeedback = 'The user declined this action.',
|
|
260
|
-
// Structured elicitation: the validated form submission the fake "relay"
|
|
261
|
-
// hands back, or null for a card that produced none.
|
|
262
|
-
structuredAnswer = null,
|
|
263
|
-
structuredTimedOut = false,
|
|
264
|
-
onAsk = null,
|
|
265
|
-
} = {}) {
|
|
266
|
-
const bridge = {
|
|
267
|
-
userInputCalls: [],
|
|
268
|
-
approvalCalls: [],
|
|
269
|
-
structuredCalls: [],
|
|
270
|
-
cancelledCount: 0,
|
|
271
|
-
async askUserInput(request, options) {
|
|
272
|
-
bridge.userInputCalls.push({ request, options });
|
|
273
|
-
if (onAsk) await onAsk(request, options);
|
|
274
|
-
const choices = Array.isArray(request?.choices) ? request.choices : [];
|
|
275
|
-
const answer = typeof userInputAnswer === 'function' ? userInputAnswer(request) : userInputAnswer;
|
|
276
|
-
return {
|
|
277
|
-
answer,
|
|
278
|
-
wasFreeform: userInputTimedOut ? true : !choices.includes(answer),
|
|
279
|
-
timedOut: userInputTimedOut,
|
|
280
|
-
};
|
|
281
|
-
},
|
|
282
|
-
async askToolApproval(request, options) {
|
|
283
|
-
bridge.approvalCalls.push({ request, options });
|
|
284
|
-
if (onAsk) await onAsk(request, options);
|
|
285
|
-
return {
|
|
286
|
-
approved: approvalTimedOut ? false : approve,
|
|
287
|
-
answer: approve ? 'Approve' : 'Deny',
|
|
288
|
-
feedback: approvalFeedback,
|
|
289
|
-
timedOut: approvalTimedOut,
|
|
290
|
-
description: 'a tool',
|
|
291
|
-
};
|
|
292
|
-
},
|
|
293
|
-
async askStructured(spec, options) {
|
|
294
|
-
bridge.structuredCalls.push({ spec, options });
|
|
295
|
-
if (onAsk) await onAsk(spec, options);
|
|
296
|
-
const answer = typeof structuredAnswer === 'function' ? structuredAnswer(spec) : structuredAnswer;
|
|
297
|
-
return {
|
|
298
|
-
structuredAnswer: structuredTimedOut ? null : answer,
|
|
299
|
-
answer: '',
|
|
300
|
-
timedOut: structuredTimedOut,
|
|
301
|
-
};
|
|
302
|
-
},
|
|
303
|
-
async cancelPendingQuestions() {
|
|
304
|
-
bridge.cancelledCount += 1;
|
|
305
|
-
return 0;
|
|
306
|
-
},
|
|
307
|
-
pendingQuestionCount: () => 0,
|
|
308
|
-
};
|
|
309
|
-
return bridge;
|
|
310
|
-
}
|
|
311
|
-
|
|
312
|
-
/**
|
|
313
|
-
* The relay-context prefix the runner prepends to every prompt, for tests that
|
|
314
|
-
* assert on the prompt. `relayToolInstructions` defaults to '' in the harness,
|
|
315
|
-
* so only the mode marker and the mode's standing instructions appear, and the
|
|
316
|
-
* instructions only on the FIRST turn of a given relay mode.
|
|
317
|
-
*/
|
|
318
|
-
export function expectedPromptPrefix(mode = 'agent', { includeInstructions = true } = {}) {
|
|
319
|
-
const marker = `[Relay mode: ${mode}]`;
|
|
320
|
-
if (!includeInstructions) return marker;
|
|
321
|
-
const instructions = {
|
|
322
|
-
agent: 'Proceed as an interactive coding agent and use tools as needed. If you need clarification, pause and ask through the web relay instead of stalling silently. These instructions remain in effect until relay mode changes.',
|
|
323
|
-
plan: 'Draft a concise plan only. Use read-only inspection tools (glob, rg, view) only when they materially improve plan quality; otherwise draft from provided context. Do not edit repository files or run mutating commands unless the user explicitly asks for implementation. If clarification is required, pause and ask through the web relay. These instructions remain in effect until relay mode changes.',
|
|
324
|
-
ask: 'Prioritize clarification questions before doing any implementation work. If the request is ambiguous or underspecified, pause and ask through the web relay before making assumptions. Do not make broad assumptions when a question would materially change the result. These instructions remain in effect until relay mode changes.',
|
|
325
|
-
autopilot: 'Act directly on the request and use tools when needed. Keep moving unless user input is truly blocking. These instructions remain in effect until relay mode changes.',
|
|
326
|
-
}[mode];
|
|
327
|
-
// The guidance turns also carry the media-embed capability block, appended
|
|
328
|
-
// by applyMediaEmbedInstructions in copilot-prompt-context.mjs.
|
|
329
|
-
return `${marker} ${instructions} ${renderMediaEmbedInstructionBlock()}`;
|
|
330
|
-
}
|
|
331
|
-
|
|
332
|
-
/** The message body with the relay-context prefix in front, as sent. */
|
|
333
|
-
export function promptWithPrefix(body, mode = 'agent', options = {}) {
|
|
334
|
-
return `${expectedPromptPrefix(mode, options)} ${body}`;
|
|
335
|
-
}
|
|
336
|
-
|
|
337
|
-
export function makeRunner({
|
|
338
|
-
stub,
|
|
339
|
-
client,
|
|
340
|
-
startWarning = null,
|
|
341
|
-
runtimeVersion = '1.0.82',
|
|
342
|
-
questionBridge = makeFakeQuestionBridge(),
|
|
343
|
-
...overrides
|
|
344
|
-
} = {}) {
|
|
345
|
-
const started = [];
|
|
346
|
-
return {
|
|
347
|
-
started,
|
|
348
|
-
questionBridge,
|
|
349
|
-
runner: createCopilotSdkSessionRunner({
|
|
350
|
-
api: stub,
|
|
351
|
-
sdkSessionId: 'conv-1',
|
|
352
|
-
cwd: '/tmp/relay-fixture-workspace',
|
|
353
|
-
resolvePathsImpl: () => FAKE_SDK_PATHS,
|
|
354
|
-
// Spawn-free and relay-free by default: no test reads the real
|
|
355
|
-
// relay-tools.md off disk or asks a relay whether previews are on.
|
|
356
|
-
relayToolInstructions: '',
|
|
357
|
-
getPreviewInstructionsImpl: () => '',
|
|
358
|
-
createQuestionBridgeImpl: () => questionBridge,
|
|
359
|
-
startClientImpl: async (options) => {
|
|
360
|
-
started.push(options);
|
|
361
|
-
return {
|
|
362
|
-
sdk: {},
|
|
363
|
-
client,
|
|
364
|
-
paths: options.paths,
|
|
365
|
-
// The real adapter never blocks client start on the version probe;
|
|
366
|
-
// it hands back a promise the runner logs when it settles.
|
|
367
|
-
versionReady: Promise.resolve({ runtimeVersion, versionSkewWarning: startWarning }),
|
|
368
|
-
};
|
|
369
|
-
},
|
|
370
|
-
...overrides,
|
|
371
|
-
}),
|
|
372
|
-
};
|
|
373
|
-
}
|
|
374
|
-
|
|
375
|
-
/**
|
|
376
|
-
* The events of a fixture up to and including the first one matching `type`.
|
|
377
|
-
* Lets a test replay "everything through session.idle" without hand-listing.
|
|
378
|
-
*/
|
|
379
|
-
export function eventsThrough(events, type) {
|
|
380
|
-
const index = events.findIndex((event) => event.type === type);
|
|
381
|
-
return index === -1 ? events.slice() : events.slice(0, index + 1);
|
|
382
|
-
}
|