@oar-sh/oar 0.9.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.
- package/.github/extensions/web-relay/README.md +190 -0
- package/.github/extensions/web-relay/extension.mjs +972 -0
- package/.github/extensions/web-relay/model-api/model-switching.mjs +338 -0
- package/.github/extensions/web-relay/polling/heartbeat.mjs +55 -0
- package/.github/extensions/web-relay/polling/polling-loop.mjs +1105 -0
- package/.github/extensions/web-relay/runtime/api-client.mjs +34 -0
- package/.github/extensions/web-relay/runtime/banner-state.mjs +126 -0
- package/.github/extensions/web-relay/runtime/config-loader.mjs +104 -0
- package/.github/extensions/web-relay/runtime/debug-log.mjs +27 -0
- package/.github/extensions/web-relay/runtime/send-and-wait-errors.mjs +170 -0
- package/.github/extensions/web-relay/runtime/session-binding.mjs +78 -0
- package/.github/extensions/web-relay/runtime/session-io.mjs +349 -0
- package/.github/extensions/web-relay/runtime/session-join-retry.mjs +44 -0
- package/.github/extensions/web-relay/runtime/session-registry.mjs +29 -0
- package/.github/extensions/web-relay/runtime/session-runtime-manager.mjs +156 -0
- package/.github/extensions/web-relay/runtime/session-sync-bridge.mjs +74 -0
- package/.github/extensions/web-relay/runtime/worker-websocket-link.mjs +325 -0
- package/.github/extensions/web-relay/runtime/workspace-root.mjs +55 -0
- package/.github/extensions/web-relay/server-lifecycle/managed-server.mjs +386 -0
- package/.github/extensions/web-relay/skills/prompt-context.mjs +160 -0
- package/.github/extensions/web-relay/skills/question-bridge.mjs +141 -0
- package/.github/extensions/web-relay/skills/question-routing-hooks.mjs +463 -0
- package/.github/extensions/web-relay/skills/reasoning-stream.mjs +285 -0
- package/.github/extensions/web-relay/skills/subagent-lifecycle.mjs +345 -0
- package/.github/extensions/web-relay/skills/tool-activity.mjs +243 -0
- package/.github/extensions/web-relay/utils/process-utils.mjs +25 -0
- package/.github/extensions/web-relay/utils/token-generation.mjs +58 -0
- package/CHANGELOG.md +25 -0
- package/LICENSE +21 -0
- package/README.md +717 -0
- package/bin/bat/oar.bat +46 -0
- package/bin/oar.js +665 -0
- package/package.json +68 -0
- package/server/README.md +1379 -0
- package/server/claude-worker/claude-attachments.mjs +134 -0
- package/server/claude-worker/claude-preview-tool.mjs +57 -0
- package/server/claude-worker/claude-sdk-adapter.mjs +420 -0
- package/server/claude-worker/claude-session-process.mjs +2391 -0
- package/server/claude-worker/claude-session-test-harness.mjs +236 -0
- package/server/claude-worker/claude-session-worker.mjs +186 -0
- package/server/claude-worker/claude-transcript-relocator.mjs +189 -0
- package/server/claude-worker/claude-turn-publisher.mjs +291 -0
- package/server/claude-worker/fixtures/workflow-run-record.json +1 -0
- package/server/claude-worker/sdk-message-normalizer.mjs +629 -0
- package/server/claude-worker/workflow-progress-digest.mjs +269 -0
- package/server/copilot-sdk-runtime.mjs +126 -0
- package/server/copilot-worker/copilot-attachments.mjs +97 -0
- package/server/copilot-worker/copilot-byok-provider.mjs +121 -0
- package/server/copilot-worker/copilot-continuation-signals.mjs +349 -0
- package/server/copilot-worker/copilot-plan-board.mjs +110 -0
- package/server/copilot-worker/copilot-prompt-context.mjs +86 -0
- package/server/copilot-worker/copilot-question-bridge.mjs +216 -0
- package/server/copilot-worker/copilot-sdk-adapter.mjs +494 -0
- package/server/copilot-worker/copilot-sdk-event-normalizer.mjs +800 -0
- package/server/copilot-worker/copilot-sdk-session-process.mjs +1729 -0
- package/server/copilot-worker/copilot-sdk-session-worker.mjs +167 -0
- package/server/copilot-worker/copilot-sdk-test-harness.mjs +329 -0
- package/server/copilot-worker/fixtures/abort-turn.json +588 -0
- package/server/copilot-worker/fixtures/ask-user-turn.json +1154 -0
- package/server/copilot-worker/fixtures/background-timer-continuation.json +445 -0
- package/server/copilot-worker/fixtures/background-timer-turn.json +497 -0
- package/server/copilot-worker/fixtures/happy-turn.json +566 -0
- package/server/copilot-worker/fixtures/quota-turn.json +647 -0
- package/server/copilot-worker/fixtures/reasoning-turn.json +625 -0
- package/server/copilot-worker/fixtures/resume-turn.json +700 -0
- package/server/copilot-worker/fixtures/subagent-turn.json +1378 -0
- package/server/copilot-worker/fixtures/tool-permission-turn.json +1293 -0
- package/server/cursor-worker/cursor-ask-user-tool.mjs +83 -0
- package/server/cursor-worker/cursor-attachments.mjs +88 -0
- package/server/cursor-worker/cursor-context-usage.mjs +85 -0
- package/server/cursor-worker/cursor-preview-tool.mjs +44 -0
- package/server/cursor-worker/cursor-sdk-adapter.mjs +554 -0
- package/server/cursor-worker/cursor-session-worker.mjs +145 -0
- package/server/cursor-worker/cursor-subagent-roster.mjs +81 -0
- package/server/cursor-worker/cursor-turn-runner.mjs +717 -0
- package/server/cursor-worker/sdk-message-normalizer.mjs +485 -0
- package/server/db-schema.mjs +932 -0
- package/server/features.mjs +106 -0
- package/server/grok-worker/acp-client.mjs +434 -0
- package/server/grok-worker/acp-host-services.mjs +277 -0
- package/server/grok-worker/grok-context-usage.mjs +77 -0
- package/server/grok-worker/grok-sdk-adapter.mjs +334 -0
- package/server/grok-worker/grok-session-worker.mjs +145 -0
- package/server/grok-worker/grok-turn-runner.mjs +523 -0
- package/server/grok-worker/sdk-message-normalizer.mjs +311 -0
- package/server/migrations/0001-add-structured-answer.mjs +80 -0
- package/server/migrations/0002-recent-workspace-roots-path-key.mjs +121 -0
- package/server/migrations/0003-push-subscriptions.mjs +87 -0
- package/server/public/app/action-confirmations.js +267 -0
- package/server/public/app/activity-replay-state.mjs +149 -0
- package/server/public/app/api-client.js +1082 -0
- package/server/public/app/ask-user-view.js +545 -0
- package/server/public/app/attachments-view.js +2112 -0
- package/server/public/app/auto-compact-window-options.mjs +59 -0
- package/server/public/app/background-tasks-view.mjs +494 -0
- package/server/public/app/bootstrap.js +4347 -0
- package/server/public/app/claude-auth-ui.js +394 -0
- package/server/public/app/claude-thinking-options.mjs +38 -0
- package/server/public/app/cli-install-ui.js +455 -0
- package/server/public/app/code-copy.mjs +85 -0
- package/server/public/app/composer-attachment-cache.mjs +151 -0
- package/server/public/app/composer-control-state.mjs +107 -0
- package/server/public/app/composer-paste.mjs +132 -0
- package/server/public/app/context-tier-options.mjs +68 -0
- package/server/public/app/context-usage-view.mjs +338 -0
- package/server/public/app/conversation-draft-timestamp-utils.mjs +11 -0
- package/server/public/app/conversation-preferences.mjs +91 -0
- package/server/public/app/conversation-provider-indicator.mjs +88 -0
- package/server/public/app/conversation-view.js +2898 -0
- package/server/public/app/cwd-menu-placement.mjs +85 -0
- package/server/public/app/cwd-picker.js +605 -0
- package/server/public/app/emoji-view.js +101 -0
- package/server/public/app/external-link-policy.mjs +80 -0
- package/server/public/app/file-preview-navigation.mjs +51 -0
- package/server/public/app/font-scaling.js +262 -0
- package/server/public/app/git-changes-view.js +339 -0
- package/server/public/app/git-diff-model.mjs +109 -0
- package/server/public/app/grok-auth-ui.js +387 -0
- package/server/public/app/image-downscale.mjs +174 -0
- package/server/public/app/in-flight-snapshot.mjs +40 -0
- package/server/public/app/infinite-loader.js +164 -0
- package/server/public/app/journal-view.js +1141 -0
- package/server/public/app/known-cwd-options.mjs +41 -0
- package/server/public/app/launch-cwd-path.mjs +23 -0
- package/server/public/app/live-message-dedupe.mjs +57 -0
- package/server/public/app/message-search-view.js +242 -0
- package/server/public/app/model-selector-options.mjs +31 -0
- package/server/public/app/model-vision-support.mjs +56 -0
- package/server/public/app/new-conversation-model-choice.mjs +67 -0
- package/server/public/app/plan-usage-view.mjs +307 -0
- package/server/public/app/preview-cards.mjs +273 -0
- package/server/public/app/preview-command.mjs +103 -0
- package/server/public/app/push-settings.js +370 -0
- package/server/public/app/pwa-install.js +456 -0
- package/server/public/app/question-schema-view.mjs +71 -0
- package/server/public/app/reasoning-effort-labels.mjs +42 -0
- package/server/public/app/relay-board-view.js +117 -0
- package/server/public/app/relay-dot-state.mjs +69 -0
- package/server/public/app/relay-error-ctas.mjs +52 -0
- package/server/public/app/relay-prompt-sanitizer.mjs +114 -0
- package/server/public/app/relay-thoughts.mjs +98 -0
- package/server/public/app/relay-watchdog-policy.mjs +29 -0
- package/server/public/app/repo-browser-preferences.mjs +79 -0
- package/server/public/app/repo-browser-tree-state.mjs +97 -0
- package/server/public/app/router.js +657 -0
- package/server/public/app/selection-guard.mjs +93 -0
- package/server/public/app/settings-modal.js +1484 -0
- package/server/public/app/settings-tabs.js +129 -0
- package/server/public/app/slash-autocomplete.mjs +188 -0
- package/server/public/app/slash-commands.mjs +183 -0
- package/server/public/app/socket-handlers.js +703 -0
- package/server/public/app/status-store.mjs +242 -0
- package/server/public/app/status-view.mjs +315 -0
- package/server/public/app/store.js +1454 -0
- package/server/public/app/stream-state.mjs +73 -0
- package/server/public/app/streaming-dom-patch.mjs +23 -0
- package/server/public/app/sync-outbox.mjs +208 -0
- package/server/public/app/tap-actions.js +62 -0
- package/server/public/app/thread-order.mjs +54 -0
- package/server/public/app/tmux-inspector-view.js +422 -0
- package/server/public/app/transcript-separators.mjs +282 -0
- package/server/public/app-icon-192.png +0 -0
- package/server/public/app-icon-512.png +0 -0
- package/server/public/app-icon.svg +4 -0
- package/server/public/favicon.ico +0 -0
- package/server/public/index.html +5743 -0
- package/server/public/manifest.webmanifest +32 -0
- package/server/public/sw.js +307 -0
- package/server/relay-exit-codes.mjs +4 -0
- package/server/relay-self-restart.mjs +170 -0
- package/server/relay-tools.md +27 -0
- package/server/relay.mjs +1026 -0
- package/server/repositories/image-conversation-repository.mjs +317 -0
- package/server/repositories/message-repository.mjs +434 -0
- package/server/repositories/question-repository.mjs +161 -0
- package/server/repositories/session-repository.mjs +320 -0
- package/server/repositories/share-visibility-statements.mjs +24 -0
- package/server/routes/ask-user-routes.mjs +292 -0
- package/server/routes/cache-routes.mjs +44 -0
- package/server/routes/git-routes.mjs +92 -0
- package/server/routes/messages-routes-test-harness.mjs +111 -0
- package/server/routes/messages-routes.mjs +7107 -0
- package/server/routes/preview-routes.mjs +108 -0
- package/server/routes/push-routes.mjs +59 -0
- package/server/routes/relay-board-routes.mjs +268 -0
- package/server/routes/sessions-routes.mjs +6150 -0
- package/server/server-runtime.mjs +7463 -0
- package/server/server.js +24 -0
- package/server/services/ask-user-routing-service.mjs +117 -0
- package/server/services/cache-rebuild-service.mjs +236 -0
- package/server/services/claude-auth-service.mjs +578 -0
- package/server/services/claude-context-usage.mjs +240 -0
- package/server/services/claude-session-root-service.mjs +228 -0
- package/server/services/cli-install-service.mjs +1152 -0
- package/server/services/cli-process-runner.mjs +181 -0
- package/server/services/cloudflared-tunnel-service.mjs +358 -0
- package/server/services/context-snapshot-service.mjs +499 -0
- package/server/services/context-usage-view.mjs +160 -0
- package/server/services/conversation-preferences-service.mjs +208 -0
- package/server/services/cursor-dashboard-usage.mjs +217 -0
- package/server/services/cursor-session-root-service.mjs +62 -0
- package/server/services/delete-archive-service.mjs +154 -0
- package/server/services/drives-path-helpers.mjs +66 -0
- package/server/services/fixtures/claude-auth-stub.sh +62 -0
- package/server/services/fixtures/cli-install-stub.sh +75 -0
- package/server/services/fixtures/grok-stub.sh +86 -0
- package/server/services/generated-image-cleanup-service.mjs +114 -0
- package/server/services/git-changes-service.mjs +195 -0
- package/server/services/github-billing-usage.mjs +133 -0
- package/server/services/grok-auth-service.mjs +593 -0
- package/server/services/grok-billing-usage.mjs +120 -0
- package/server/services/image-context-service.mjs +106 -0
- package/server/services/image-operation-service.mjs +135 -0
- package/server/services/keyed-mutex-service.mjs +82 -0
- package/server/services/mime-sniffer.mjs +118 -0
- package/server/services/oar-cli-helpers.mjs +109 -0
- package/server/services/oar-state-migration-service.mjs +243 -0
- package/server/services/plan-usage-claude.mjs +387 -0
- package/server/services/plan-usage-contract.mjs +271 -0
- package/server/services/plan-usage-copilot.mjs +421 -0
- package/server/services/plan-usage-cursor.mjs +566 -0
- package/server/services/plan-usage-grok.mjs +436 -0
- package/server/services/plan-usage-service.mjs +487 -0
- package/server/services/preview-health-probe.mjs +82 -0
- package/server/services/preview-proxy-server.mjs +498 -0
- package/server/services/preview-registry-service.mjs +457 -0
- package/server/services/preview-static-handler.mjs +237 -0
- package/server/services/provider-model-selection.mjs +91 -0
- package/server/services/provider-reasoning-effort.mjs +94 -0
- package/server/services/push-active-device-service.mjs +57 -0
- package/server/services/push-dispatch-service.mjs +459 -0
- package/server/services/rate-limit-service.mjs +63 -0
- package/server/services/relay-bridge-owner-service.mjs +84 -0
- package/server/services/relay-cli-launcher-service.mjs +209 -0
- package/server/services/relay-prompt-sanitizer.mjs +137 -0
- package/server/services/relay-queue-gate-service.mjs +38 -0
- package/server/services/relay-restart-orchestrator-service.mjs +935 -0
- package/server/services/relay-singleton-guard.mjs +124 -0
- package/server/services/safe-served-content.mjs +57 -0
- package/server/services/sdk-session-import-service.mjs +271 -0
- package/server/services/sdk-session-sync-service.mjs +207 -0
- package/server/services/session-history-refresh-service.mjs +497 -0
- package/server/services/session-transcript-service.mjs +419 -0
- package/server/services/session-worker-launch-service.mjs +786 -0
- package/server/services/session-worker-process-service.mjs +319 -0
- package/server/services/session-worker-registry-service.mjs +194 -0
- package/server/services/session-worker-stop-service.mjs +141 -0
- package/server/services/session-worker-supervisor-service.mjs +898 -0
- package/server/services/session-worker-websocket-service.mjs +483 -0
- package/server/services/ssh-tunnel-manager-service.mjs +423 -0
- package/server/services/status-event-service.mjs +158 -0
- package/server/services/tmux-inspector-access-policy.mjs +77 -0
- package/server/services/tmux-inspector-socket-service.mjs +191 -0
- package/server/services/tmux-inspector-stream-service.mjs +552 -0
- package/server/services/tunnel-worker-path-guard.mjs +107 -0
- package/server/services/upload-sweep.mjs +60 -0
- package/server/services/usage-snapshot-helpers.mjs +209 -0
- package/server/services/windows-autostart-service.mjs +115 -0
- package/server/services/workspace-root-defaults-service.mjs +34 -0
- package/server/services/workspace-root-path-policy.mjs +196 -0
- package/server/services/workspace-root-relaunch-service.mjs +132 -0
- package/server/services/workspace-symlink-guard.mjs +54 -0
- package/server/tools/capture-proxy.mjs +173 -0
- package/server/tty-console-bootstrap.mjs +75 -0
- package/server/uploads/README.md +36 -0
- package/server/uploads/image_input.md +540 -0
- package/server/windows-terminal-launcher.mjs +29 -0
- package/server/workspace-root.mjs +87 -0
- package/shared/ask-user-bridge.mjs +131 -0
- package/shared/auto-compact-window.mjs +116 -0
- package/shared/background-task-timeout.mjs +52 -0
- package/shared/claude-thinking.mjs +101 -0
- package/shared/context-window-fallbacks.mjs +148 -0
- package/shared/control-poller.mjs +115 -0
- package/shared/empty-turn-completion.mjs +20 -0
- package/shared/model-catalog-freshness.mjs +11 -0
- package/shared/model-descriptors.mjs +118 -0
- package/shared/model-id.mjs +147 -0
- package/shared/model-refresh.mjs +24 -0
- package/shared/openai-reasoning.mjs +13 -0
- package/shared/plan-lines.mjs +13 -0
- package/shared/preview-instructions.mjs +77 -0
- package/shared/preview-tool-core.mjs +191 -0
- package/shared/provider-routing.mjs +29 -0
- package/shared/question-schema.mjs +315 -0
- package/shared/question-timeout.mjs +18 -0
- package/shared/stream-emit-gating.mjs +28 -0
- package/shared/subagent-run-id.mjs +17 -0
- package/shared/thought-cap.mjs +13 -0
- package/shared/tool-activity.mjs +98 -0
- package/shared/turn-ceiling.mjs +53 -0
- package/shared/worker-bootstrap.mjs +44 -0
- package/shared/worker-crash-guard.mjs +60 -0
|
@@ -0,0 +1,349 @@
|
|
|
1
|
+
// Pure signal extraction for self-initiated (continuation) turns.
|
|
2
|
+
//
|
|
3
|
+
// The Copilot runtime can start a turn with nobody asking it to. The shape that
|
|
4
|
+
// motivated this module, captured live on 2026-08-31 (session `10a1a9ad`,
|
|
5
|
+
// gpt-5.6-luna, "set a timer to 1 minute"):
|
|
6
|
+
//
|
|
7
|
+
// user prompt → bash{mode:"async", detach:true} → tool.execution_complete
|
|
8
|
+
// "<command started in detached background with shellId: 1>"
|
|
9
|
+
// → assistant.message "Timer set for 1 minute…" → session.idle
|
|
10
|
+
// ── the worker settles the row here, correctly ──
|
|
11
|
+
// 60s later: system.notification{kind:{type:"shell_detached_completed", shellId:"1"}}
|
|
12
|
+
// → assistant.turn_start → read_bash{shellId:"1"}
|
|
13
|
+
// → assistant.message "⏰ The 1-minute timer fired." → session.idle
|
|
14
|
+
//
|
|
15
|
+
// Everything after the notification arrived with **no active turn**, so the
|
|
16
|
+
// worker dropped it and the user never saw the reply. Worse, the 10-minute idle
|
|
17
|
+
// shutdown would have killed the detached shell outright had the timer been
|
|
18
|
+
// longer. This module supplies the three signals the runner needs to fix that,
|
|
19
|
+
// with no I/O and no SDK import so they can be unit-tested against real event
|
|
20
|
+
// payloads:
|
|
21
|
+
//
|
|
22
|
+
// 1. `createBackgroundShellTracker` — which detached shells are still live
|
|
23
|
+
// (lifecycle pinning), and when one settles (a continuation is due).
|
|
24
|
+
// 2. `isContinuationOpeningEvent` — the runtime has started work of its own.
|
|
25
|
+
// 3. `createReplayGate` — the events a `session.resume` replays are history,
|
|
26
|
+
// not new work, and must never open a continuation row.
|
|
27
|
+
//
|
|
28
|
+
// ## Why detached shells rather than `session.background_tasks_changed`
|
|
29
|
+
//
|
|
30
|
+
// `BackgroundTasksChangedEvent` exists and fires (~23 times during a single
|
|
31
|
+
// bash call), but its `data` is documented as an **empty payload** —
|
|
32
|
+
// "indicating background task state changed", with no task list, no id and no
|
|
33
|
+
// state. It cannot answer "is anything still running?", which is the only
|
|
34
|
+
// question lifecycle pinning has. The typed, id-carrying signals are the shell
|
|
35
|
+
// ones, so those are what this tracks:
|
|
36
|
+
//
|
|
37
|
+
// - opened: a `bash` `tool.execution_complete` whose telemetry says
|
|
38
|
+
// `detached: "true"` and/or whose result text names the new shell id;
|
|
39
|
+
// - settled: `system.notification` with a `shell_detached_completed` /
|
|
40
|
+
// `shell_completed` `kind` — a discriminated union carrying `shellId`;
|
|
41
|
+
// - closed by hand: a `read_bash` result reporting the exit code, or a
|
|
42
|
+
// successful `stop_bash`.
|
|
43
|
+
//
|
|
44
|
+
// The set is a best-effort superset-free view: a shell this module never learns
|
|
45
|
+
// about simply is not pinned (the old behaviour), and a shell it never learns
|
|
46
|
+
// the end of is released by the runner's cap. Neither can wedge a turn.
|
|
47
|
+
|
|
48
|
+
/** `system.notification` kinds that mean "a shell this session started is done". */
|
|
49
|
+
export const SHELL_SETTLED_NOTIFICATION_KINDS = Object.freeze([
|
|
50
|
+
'shell_detached_completed',
|
|
51
|
+
'shell_completed',
|
|
52
|
+
]);
|
|
53
|
+
|
|
54
|
+
/** Tools that address an already-running shell by id. */
|
|
55
|
+
export const SHELL_HANDLE_TOOLS = Object.freeze(['read_bash', 'write_bash', 'stop_bash']);
|
|
56
|
+
|
|
57
|
+
// `<command started in detached background with shellId: 1>`
|
|
58
|
+
const SHELL_OPENED_RE = /detached\s+(?:background|command)[^<>]*?shellId:\s*([^\s>,]+)/i;
|
|
59
|
+
// `<detached command with shellId: 1 completed with exit code 0>`
|
|
60
|
+
const SHELL_EXITED_RE = /shellId:\s*([^\s>,]+)[^<>]*?completed with exit code/i;
|
|
61
|
+
|
|
62
|
+
/** Cap on the toolCallId → shell bookkeeping, so a long session cannot grow it forever. */
|
|
63
|
+
const TOOL_CALL_MEMORY = 200;
|
|
64
|
+
|
|
65
|
+
function eventData(event) {
|
|
66
|
+
return event && typeof event.data === 'object' && event.data ? event.data : {};
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function toolResultText(data) {
|
|
70
|
+
const result = data?.result;
|
|
71
|
+
if (!result || typeof result !== 'object') return '';
|
|
72
|
+
return `${String(result.content || '')}\n${String(result.detailedContent || '')}`;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function normalizeShellId(value) {
|
|
76
|
+
const id = String(value ?? '').trim();
|
|
77
|
+
return id && id !== 'undefined' && id !== 'null' ? id : '';
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* The events that mean "the runtime has started doing something".
|
|
82
|
+
*
|
|
83
|
+
* An allowlist, not a denylist: the SDK event set grows between CLI releases
|
|
84
|
+
* (a live probe already found an undocumented `subagent.configured`), and the
|
|
85
|
+
* cost of the two directions is not symmetric. A missing opener costs one
|
|
86
|
+
* dropped continuation — today's behaviour. A spurious opener creates a
|
|
87
|
+
* synthetic queue row for a housekeeping event and leaves it open until the
|
|
88
|
+
* stall watchdog fails it, in the user's transcript.
|
|
89
|
+
*
|
|
90
|
+
* `session.idle` / `session.error` are deliberately absent: they terminate
|
|
91
|
+
* turns, and a stray terminator arriving with no turn must stay a no-op rather
|
|
92
|
+
* than open a row it would immediately close. So are `session.start`,
|
|
93
|
+
* `session.resume`, `session.shutdown`, `session.background_tasks_changed`,
|
|
94
|
+
* `pending_messages.modified`, `capabilities.changed` and the `model.*` family,
|
|
95
|
+
* which is all connection bookkeeping.
|
|
96
|
+
*/
|
|
97
|
+
export const CONTINUATION_OPENING_EVENT_TYPES = Object.freeze([
|
|
98
|
+
// The runtime picked up a prompt on its own (a resume with
|
|
99
|
+
// `continuePendingWork`, or a prompt queued inside the runtime that outlived
|
|
100
|
+
// the interaction it was sent into).
|
|
101
|
+
'user.message',
|
|
102
|
+
'assistant.turn_start',
|
|
103
|
+
'assistant.message_start',
|
|
104
|
+
'assistant.message_delta',
|
|
105
|
+
'assistant.message',
|
|
106
|
+
'assistant.reasoning',
|
|
107
|
+
'assistant.reasoning_delta',
|
|
108
|
+
'assistant.intent',
|
|
109
|
+
'tool.execution_start',
|
|
110
|
+
'tool.user_requested',
|
|
111
|
+
'permission.requested',
|
|
112
|
+
'user_input.requested',
|
|
113
|
+
'elicitation.requested',
|
|
114
|
+
'exit_plan_mode.requested',
|
|
115
|
+
'subagent.started',
|
|
116
|
+
'skill.invoked',
|
|
117
|
+
]);
|
|
118
|
+
|
|
119
|
+
const OPENING_TYPES = new Set(CONTINUATION_OPENING_EVENT_TYPES);
|
|
120
|
+
|
|
121
|
+
export function isContinuationOpeningEvent(event) {
|
|
122
|
+
return OPENING_TYPES.has(String(event?.type || ''));
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Suppress the historical events a `session.resume` replays through the live
|
|
127
|
+
* event callback.
|
|
128
|
+
*
|
|
129
|
+
* `ResumeData` gives both halves of the discriminator, and they are used
|
|
130
|
+
* together because either alone is unsafe:
|
|
131
|
+
*
|
|
132
|
+
* - `resumeTime` — "ISO 8601 timestamp when the session was resumed". A
|
|
133
|
+
* persisted event carries its ORIGINAL `timestamp` (verified against a real
|
|
134
|
+
* `events.jsonl`), so anything older than `resumeTime` is history. Alone,
|
|
135
|
+
* this would suppress forever if the runtime ever emitted a stale timestamp.
|
|
136
|
+
* - `eventCount` — "Total number of persisted events in the session at the
|
|
137
|
+
* time of resume". That is an exact ceiling on how many events a replay can
|
|
138
|
+
* produce. Alone, a count is fragile: ephemeral events interleave with the
|
|
139
|
+
* replay (the resume fixture has 20+ of them in the first second), and
|
|
140
|
+
* miscounting by one would eat a live event or admit a replayed one.
|
|
141
|
+
*
|
|
142
|
+
* So: suppress events older than `resumeTime`, at most `eventCount` of them,
|
|
143
|
+
* and disarm at the first event that is not older — the replay is contiguous
|
|
144
|
+
* and precedes live traffic, so the first live event proves it is over.
|
|
145
|
+
*
|
|
146
|
+
* A resume with no `resumeTime`, or `eventCount: 0`, arms nothing at all: a
|
|
147
|
+
* fresh session has no history to replay, and guessing would be worse than the
|
|
148
|
+
* status quo.
|
|
149
|
+
*/
|
|
150
|
+
export function createReplayGate() {
|
|
151
|
+
let window = null;
|
|
152
|
+
|
|
153
|
+
function arm(event) {
|
|
154
|
+
const data = eventData(event);
|
|
155
|
+
const resumeTime = Date.parse(String(data.resumeTime || ''));
|
|
156
|
+
const eventCount = Number(data.eventCount);
|
|
157
|
+
if (!Number.isFinite(resumeTime) || !Number.isFinite(eventCount) || eventCount <= 0) {
|
|
158
|
+
window = null;
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
window = { resumeTime, remaining: Math.floor(eventCount) };
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/** True when `event` is a replayed historical event and must be dropped. */
|
|
165
|
+
function isReplay(event) {
|
|
166
|
+
if (String(event?.type || '') === 'session.resume') {
|
|
167
|
+
// The resume event itself is live — it is the thing that arms the window.
|
|
168
|
+
arm(event);
|
|
169
|
+
return false;
|
|
170
|
+
}
|
|
171
|
+
if (!window) return false;
|
|
172
|
+
const timestamp = Date.parse(String(event?.timestamp || ''));
|
|
173
|
+
if (!Number.isFinite(timestamp)) return false;
|
|
174
|
+
if (timestamp >= window.resumeTime) {
|
|
175
|
+
window = null;
|
|
176
|
+
return false;
|
|
177
|
+
}
|
|
178
|
+
window.remaining -= 1;
|
|
179
|
+
if (window.remaining <= 0) window = null;
|
|
180
|
+
return true;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
return {
|
|
184
|
+
isReplay,
|
|
185
|
+
reset: () => { window = null; },
|
|
186
|
+
/** Test seam: whether a replay window is currently open. */
|
|
187
|
+
armed: () => !!window,
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Track the detached shells this session has running.
|
|
193
|
+
*
|
|
194
|
+
* `observe(event)` returns what the event changed, so the caller can pin the
|
|
195
|
+
* runtime's lifecycle and note that a continuation is due without re-parsing.
|
|
196
|
+
*/
|
|
197
|
+
export function createBackgroundShellTracker({ now = () => Date.now() } = {}) {
|
|
198
|
+
const shells = new Map(); // shellId -> { shellId, description, startedAt }
|
|
199
|
+
const toolCalls = new Map(); // toolCallId -> { toolName, shellId, description }
|
|
200
|
+
|
|
201
|
+
function rememberToolCall(toolCallId, entry) {
|
|
202
|
+
if (!toolCallId) return;
|
|
203
|
+
toolCalls.set(toolCallId, entry);
|
|
204
|
+
// Bounded: a long conversation makes thousands of tool calls and only the
|
|
205
|
+
// most recent handful are ever looked up.
|
|
206
|
+
while (toolCalls.size > TOOL_CALL_MEMORY) {
|
|
207
|
+
const oldest = toolCalls.keys().next().value;
|
|
208
|
+
toolCalls.delete(oldest);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function open(shellId, description) {
|
|
213
|
+
const id = normalizeShellId(shellId);
|
|
214
|
+
if (!id || shells.has(id)) return null;
|
|
215
|
+
const shell = { shellId: id, description: String(description || '').trim(), startedAt: now() };
|
|
216
|
+
shells.set(id, shell);
|
|
217
|
+
return shell;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function close(shellId) {
|
|
221
|
+
const id = normalizeShellId(shellId);
|
|
222
|
+
if (!id) return null;
|
|
223
|
+
const shell = shells.get(id) || null;
|
|
224
|
+
shells.delete(id);
|
|
225
|
+
// A shell can settle before this tracker ever saw it open (the worker
|
|
226
|
+
// reconnected mid-flight); report the id anyway so the caller still knows a
|
|
227
|
+
// continuation is due.
|
|
228
|
+
return shell || { shellId: id, description: '', startedAt: 0 };
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* `heralded` is true only for the `system.notification` path.
|
|
233
|
+
*
|
|
234
|
+
* That notification is the runtime telling ITSELF a shell finished, and it is
|
|
235
|
+
* what makes it re-invoke the model — so it, and only it, means "a
|
|
236
|
+
* continuation is imminent". A `read_bash` that reports an exit code closes
|
|
237
|
+
* the same shell, but it happens INSIDE a turn that already knows: treating
|
|
238
|
+
* it as a herald would hold the runtime open for the grace window after every
|
|
239
|
+
* ordinary background command.
|
|
240
|
+
*/
|
|
241
|
+
function observe(event) {
|
|
242
|
+
const type = String(event?.type || '');
|
|
243
|
+
const data = eventData(event);
|
|
244
|
+
const opened = [];
|
|
245
|
+
const settled = [];
|
|
246
|
+
let heralded = false;
|
|
247
|
+
|
|
248
|
+
if (type === 'tool.execution_start') {
|
|
249
|
+
const toolCallId = String(data.toolCallId || '').trim();
|
|
250
|
+
const toolName = String(data.toolName || '').trim();
|
|
251
|
+
const args = data.arguments && typeof data.arguments === 'object' ? data.arguments : {};
|
|
252
|
+
rememberToolCall(toolCallId, {
|
|
253
|
+
toolName,
|
|
254
|
+
shellId: normalizeShellId(args.shellId),
|
|
255
|
+
description: String(args.description || '').trim(),
|
|
256
|
+
});
|
|
257
|
+
return { opened, settled, heralded };
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
if (type === 'tool.execution_complete') {
|
|
261
|
+
const toolCallId = String(data.toolCallId || '').trim();
|
|
262
|
+
const call = toolCalls.get(toolCallId) || null;
|
|
263
|
+
toolCalls.delete(toolCallId);
|
|
264
|
+
const text = toolResultText(data);
|
|
265
|
+
const detached = String(data.toolTelemetry?.properties?.detached || '') === 'true';
|
|
266
|
+
|
|
267
|
+
// The exit report wins over the open report: a `read_bash` whose result
|
|
268
|
+
// says the command exited is a close, even though its text also matches
|
|
269
|
+
// the "detached … shellId" shape.
|
|
270
|
+
const exited = SHELL_EXITED_RE.exec(text);
|
|
271
|
+
if (exited) {
|
|
272
|
+
const shell = close(exited[1]);
|
|
273
|
+
if (shell) settled.push(shell);
|
|
274
|
+
return { opened, settled, heralded };
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
if (call?.toolName === 'stop_bash' && data.success !== false) {
|
|
278
|
+
const shell = close(call.shellId);
|
|
279
|
+
if (shell) settled.push(shell);
|
|
280
|
+
return { opened, settled, heralded };
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
const started = SHELL_OPENED_RE.exec(text);
|
|
284
|
+
if (started && data.success !== false) {
|
|
285
|
+
const shell = open(started[1], call?.description);
|
|
286
|
+
if (shell) opened.push(shell);
|
|
287
|
+
return { opened, settled, heralded };
|
|
288
|
+
}
|
|
289
|
+
// Telemetry says detached but the result text named no id — nothing to
|
|
290
|
+
// key on, so it is deliberately not tracked rather than tracked wrongly.
|
|
291
|
+
return { opened, settled, heralded };
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
if (type === 'system.notification') {
|
|
295
|
+
const kind = data.kind && typeof data.kind === 'object' ? data.kind : {};
|
|
296
|
+
if (!SHELL_SETTLED_NOTIFICATION_KINDS.includes(String(kind.type || ''))) {
|
|
297
|
+
return { opened, settled, heralded };
|
|
298
|
+
}
|
|
299
|
+
// The runtime is about to re-invoke the model off the back of this.
|
|
300
|
+
heralded = true;
|
|
301
|
+
const shell = close(kind.shellId);
|
|
302
|
+
if (shell) {
|
|
303
|
+
settled.push({
|
|
304
|
+
...shell,
|
|
305
|
+
description: shell.description || String(kind.description || '').trim(),
|
|
306
|
+
});
|
|
307
|
+
}
|
|
308
|
+
return { opened, settled, heralded };
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
return { opened, settled, heralded };
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
return {
|
|
315
|
+
observe,
|
|
316
|
+
size: () => shells.size,
|
|
317
|
+
live: () => [...shells.values()],
|
|
318
|
+
/**
|
|
319
|
+
* Drop every shell that has been running longer than `capMs`, so a forgotten
|
|
320
|
+
* background command cannot pin the runtime forever. Returns what it forgot.
|
|
321
|
+
*/
|
|
322
|
+
expireOlderThan(capMs) {
|
|
323
|
+
if (!(capMs > 0)) return [];
|
|
324
|
+
const deadline = now() - capMs;
|
|
325
|
+
const expired = [];
|
|
326
|
+
for (const shell of [...shells.values()]) {
|
|
327
|
+
if (shell.startedAt <= deadline) {
|
|
328
|
+
shells.delete(shell.shellId);
|
|
329
|
+
expired.push(shell);
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
return expired;
|
|
333
|
+
},
|
|
334
|
+
reset() {
|
|
335
|
+
shells.clear();
|
|
336
|
+
toolCalls.clear();
|
|
337
|
+
},
|
|
338
|
+
};
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
/** A one-line transcript note for a settled shell, posted into its continuation. */
|
|
342
|
+
export function describeSettledShell(shell) {
|
|
343
|
+
const id = normalizeShellId(shell?.shellId);
|
|
344
|
+
const description = String(shell?.description || '').trim();
|
|
345
|
+
if (!id) return '';
|
|
346
|
+
return description
|
|
347
|
+
? `Background shell ${id} finished: ${description}`
|
|
348
|
+
: `Background shell ${id} finished`;
|
|
349
|
+
}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
// The `plan_ready` board: the card that gives the UI its autopilot handoff
|
|
2
|
+
// buttons when a plan-mode turn produces a plan.
|
|
3
|
+
//
|
|
4
|
+
// The payload is byte-compatible with the Claude, Cursor and Grok workers'
|
|
5
|
+
// (`PLAN_BOARD_ACTIONS`, `boardType`, `title`, `recommendedAction`, `context`
|
|
6
|
+
// shape), because the UI renders whatever `actions` the board carries and the
|
|
7
|
+
// relay's action handler maps those ids onto relay modes. Diverging here would
|
|
8
|
+
// mean a Copilot plan card whose buttons behave differently from every other
|
|
9
|
+
// provider's.
|
|
10
|
+
//
|
|
11
|
+
// Copilot has something the Cursor and Grok workers do not: a real
|
|
12
|
+
// `onExitPlanModeRequest` hook, so the plan text can be taken from the runtime
|
|
13
|
+
// at the moment the agent finishes planning rather than only guessed from the
|
|
14
|
+
// shape of the final prose. Both paths are wired — the hook is primary, the
|
|
15
|
+
// text heuristic is the fallback — which is the Claude worker's arrangement.
|
|
16
|
+
import { countPlanLikeLines } from '../../shared/plan-lines.mjs';
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Kept identical across workers. `exit_only` dismisses; `autopilot` and
|
|
20
|
+
* `interactive` queue a follow-up user message in the mapped relay mode.
|
|
21
|
+
*/
|
|
22
|
+
export const PLAN_BOARD_ACTIONS = [
|
|
23
|
+
{ id: 'autopilot', label: 'Implement in autopilot', mode: 'autopilot' },
|
|
24
|
+
{ id: 'interactive', label: 'Stop here and prompt myself', mode: 'agent' },
|
|
25
|
+
{ id: 'exit_only', label: 'Stop here', mode: 'agent' },
|
|
26
|
+
];
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* How many list-ish lines a final message needs before it is treated as a plan.
|
|
30
|
+
* Same threshold as the siblings; `countPlanLikeLines` is the shared predicate.
|
|
31
|
+
*/
|
|
32
|
+
export const PLAN_LINE_THRESHOLD = 2;
|
|
33
|
+
|
|
34
|
+
/** Returns null when there is nothing worth posting — the caller then skips. */
|
|
35
|
+
export function buildCopilotPlanReadyBoardPayload({
|
|
36
|
+
message,
|
|
37
|
+
planText = '',
|
|
38
|
+
source = 'plan-mode-fallback',
|
|
39
|
+
} = {}) {
|
|
40
|
+
const summary = String(planText || '').trim();
|
|
41
|
+
if (!summary) return null;
|
|
42
|
+
return {
|
|
43
|
+
queueId: message?.id,
|
|
44
|
+
messageId: message?.id,
|
|
45
|
+
conversationId: message?.conversationId,
|
|
46
|
+
mode: message?.relayMode || 'agent',
|
|
47
|
+
boardType: 'plan_ready',
|
|
48
|
+
title: 'Plan ready for review',
|
|
49
|
+
body: summary,
|
|
50
|
+
actions: PLAN_BOARD_ACTIONS,
|
|
51
|
+
recommendedAction: null,
|
|
52
|
+
context: {
|
|
53
|
+
source,
|
|
54
|
+
queueMessageId: message?.id || null,
|
|
55
|
+
conversationId: message?.conversationId || null,
|
|
56
|
+
relayMode: message?.relayMode || 'agent',
|
|
57
|
+
},
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Should the fallback path post a board for this finished turn?
|
|
63
|
+
*
|
|
64
|
+
* Gated on plan AND ask, which is one mode wider than the Cursor worker (plan
|
|
65
|
+
* only). The board's real precondition is "the work is described but not
|
|
66
|
+
* started", and in plan mode that is guaranteed — mutating tools are denied
|
|
67
|
+
* outright. In ask mode it is NOT: the user can approve a tool, so a turn there
|
|
68
|
+
* can genuinely do the work and then summarise it in bullets. Offering
|
|
69
|
+
* "Implement in autopilot" for work already done would be worse than offering
|
|
70
|
+
* nothing, so ask mode additionally requires that nothing was actually
|
|
71
|
+
* approved (`acted`).
|
|
72
|
+
*/
|
|
73
|
+
export function shouldPostPlanBoard({
|
|
74
|
+
relayMode,
|
|
75
|
+
finalText,
|
|
76
|
+
alreadyPosted = false,
|
|
77
|
+
acted = false,
|
|
78
|
+
} = {}) {
|
|
79
|
+
if (alreadyPosted) return false;
|
|
80
|
+
const mode = String(relayMode || '').trim().toLowerCase();
|
|
81
|
+
if (mode !== 'plan' && mode !== 'ask') return false;
|
|
82
|
+
if (mode === 'ask' && acted) return false;
|
|
83
|
+
return countPlanLikeLines(finalText) >= PLAN_LINE_THRESHOLD;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* The text the runtime handed to `onExitPlanModeRequest`. `planContent` is the
|
|
88
|
+
* full plan when the runtime has it; `summary` is always present.
|
|
89
|
+
*/
|
|
90
|
+
export function planTextFromExitRequest(request) {
|
|
91
|
+
return String(request?.planContent || request?.summary || '').trim();
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Returned to the runtime after a board is posted.
|
|
96
|
+
*
|
|
97
|
+
* `approved: false` deliberately — approving exit-plan-mode tells the runtime
|
|
98
|
+
* the user accepted the plan, and the SAME turn then rolls straight into
|
|
99
|
+
* implementing it while the board is still sitting unanswered in the UI. The
|
|
100
|
+
* feedback explains the situation so the agent closes out instead of treating
|
|
101
|
+
* the refusal as an obstacle to route around. Mirrors the Claude worker's
|
|
102
|
+
* `EXIT_PLAN_DENY_MESSAGE`.
|
|
103
|
+
*/
|
|
104
|
+
export const EXIT_PLAN_BOARD_POSTED_FEEDBACK =
|
|
105
|
+
'Plan received — it is shown to the user in the relay for review. End the turn now with a short '
|
|
106
|
+
+ 'closing message and do not start implementing; the user\'s decision will arrive as a new message.';
|
|
107
|
+
|
|
108
|
+
export const EXIT_PLAN_NO_BOARD_FEEDBACK =
|
|
109
|
+
'Plan received, but it could not be shown for review. Restate the complete plan as your final '
|
|
110
|
+
+ 'message, then end the turn without implementing; the user\'s decision will arrive as a new message.';
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
// Relay prompt context for the SDK worker: the mode marker, the mode's
|
|
2
|
+
// standing instructions, the relay tool guidance, and the live preview-lane
|
|
3
|
+
// block.
|
|
4
|
+
//
|
|
5
|
+
// This mirrors the extension's `createRelayPromptBuilder`
|
|
6
|
+
// (`.github/extensions/web-relay/skills/prompt-context.mjs`) and reuses its
|
|
7
|
+
// text verbatim through `buildModePrompt`, so a conversation reads the same
|
|
8
|
+
// guidance whichever engine ran it. It does NOT reuse the extension's builder
|
|
9
|
+
// wholesale for one reason: that builder also composes the message BODY via its
|
|
10
|
+
// own `buildPrompt`, which renders attachments as a text note. This worker
|
|
11
|
+
// passes real `MessageOptions.attachments` to the runtime
|
|
12
|
+
// (`buildCopilotMessageOptions`), so only the PREFIX is wanted here.
|
|
13
|
+
//
|
|
14
|
+
// Why the relay tool doc applies to this worker at all: `server/relay-tools.md`
|
|
15
|
+
// tells the model to ask through `ask_user` rather than in plain prose, and to
|
|
16
|
+
// restart the relay through the authenticated localhost API. Both are now true
|
|
17
|
+
// on the SDK path — `onUserInputRequest` is bridged to the relay question
|
|
18
|
+
// cards. The preview section is the part that must be live rather than static,
|
|
19
|
+
// which is exactly what `applyPreviewInstructions` swaps in.
|
|
20
|
+
import path from 'path';
|
|
21
|
+
import { fileURLToPath } from 'url';
|
|
22
|
+
|
|
23
|
+
import {
|
|
24
|
+
applyPreviewInstructions,
|
|
25
|
+
createPreviewInstructionsProvider,
|
|
26
|
+
} from '../../shared/preview-instructions.mjs';
|
|
27
|
+
import { buildModePrompt } from '../../.github/extensions/web-relay/skills/prompt-context.mjs';
|
|
28
|
+
import { loadRelayInstructionsFromFile } from '../../.github/extensions/web-relay/runtime/config-loader.mjs';
|
|
29
|
+
|
|
30
|
+
export { createPreviewInstructionsProvider, loadRelayInstructionsFromFile };
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Read `server/relay-tools.md`, honouring the same `COPILOT_WEB_RELAY_TOOLS`
|
|
34
|
+
* override the extension uses.
|
|
35
|
+
*
|
|
36
|
+
* The path is resolved relative to THIS module rather than through the
|
|
37
|
+
* extension's `resolveRelayPaths`, whose `../../../server` walk is anchored to
|
|
38
|
+
* the extension's own directory depth and resolves to the wrong place from
|
|
39
|
+
* `server/copilot-worker/`. A missing file yields '' (the loader swallows the
|
|
40
|
+
* error), which degrades to "no tool guidance", never to a failed turn.
|
|
41
|
+
*/
|
|
42
|
+
export function loadDefaultRelayToolInstructions({ env = process.env } = {}) {
|
|
43
|
+
const override = String(env.COPILOT_WEB_RELAY_TOOLS || '').trim();
|
|
44
|
+
const toolsPath = override
|
|
45
|
+
|| path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..', 'relay-tools.md');
|
|
46
|
+
return loadRelayInstructionsFromFile(toolsPath);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Build the per-turn relay context prefix.
|
|
51
|
+
*
|
|
52
|
+
* The heavy guidance rides along only when the relay mode CHANGED, matching the
|
|
53
|
+
* extension: repeat turns in one mode stay cheap, and the model is not re-read
|
|
54
|
+
* its standing instructions every message. The mode marker itself is always
|
|
55
|
+
* present.
|
|
56
|
+
*
|
|
57
|
+
* `agentMode` is threaded to the runtime natively as well
|
|
58
|
+
* (`copilotAgentModeForRelayMode`); the text is not a substitute for it but a
|
|
59
|
+
* reinforcement, same as on the extension path.
|
|
60
|
+
*/
|
|
61
|
+
export function createCopilotPromptContextBuilder({
|
|
62
|
+
toolInstructions = '',
|
|
63
|
+
getPreviewInstructions = null,
|
|
64
|
+
} = {}) {
|
|
65
|
+
let lastPromptedRelayMode = null;
|
|
66
|
+
return async function buildRelayContextPrefix(message) {
|
|
67
|
+
const relayMode = String(message?.relayMode || 'agent').trim().toLowerCase() || 'agent';
|
|
68
|
+
const includeInstructions = lastPromptedRelayMode !== relayMode;
|
|
69
|
+
let instructions = '';
|
|
70
|
+
if (includeInstructions) {
|
|
71
|
+
// Advisory: a preview lookup that fails costs the block, never the turn.
|
|
72
|
+
const previewBlock = typeof getPreviewInstructions === 'function'
|
|
73
|
+
? await Promise.resolve().then(getPreviewInstructions).catch(() => '')
|
|
74
|
+
: '';
|
|
75
|
+
instructions = applyPreviewInstructions(toolInstructions, previewBlock);
|
|
76
|
+
}
|
|
77
|
+
const prefix = buildModePrompt(relayMode, instructions, { includeInstructions });
|
|
78
|
+
lastPromptedRelayMode = relayMode;
|
|
79
|
+
return prefix;
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** Prepend the relay context prefix to a message body built elsewhere. */
|
|
84
|
+
export function withRelayContext(prefix, body) {
|
|
85
|
+
return [String(prefix || '').trim(), String(body || '').trim()].filter(Boolean).join(' ');
|
|
86
|
+
}
|