@github/copilot-sdk 1.0.15 → 1.0.16
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/README.md +136 -0
- package/dist/cjs/cliVersion.js +1 -1
- package/dist/cjs/client.js +319 -8
- package/dist/cjs/generated/rpc.js +116 -2
- package/dist/cjs/host.js +61 -0
- package/dist/cjs/index.js +3 -0
- package/dist/cliVersion.d.ts +1 -1
- package/dist/cliVersion.js +1 -1
- package/dist/client.d.ts +27 -1
- package/dist/client.js +316 -5
- package/dist/generated/rpc.d.ts +997 -14
- package/dist/generated/rpc.js +116 -2
- package/dist/generated/session-events.d.ts +81 -1
- package/dist/host.d.ts +86 -0
- package/dist/host.js +37 -0
- package/dist/index.d.ts +3 -1
- package/dist/index.js +4 -0
- package/dist/types.d.ts +3 -0
- package/package.json +11 -10
package/README.md
CHANGED
|
@@ -37,6 +37,139 @@ verify all eight platforms.
|
|
|
37
37
|
npm install @github/copilot-sdk
|
|
38
38
|
```
|
|
39
39
|
|
|
40
|
+
## Runtime-supervised AHP host (experimental)
|
|
41
|
+
|
|
42
|
+
`startAhpHost()` exposes copilotd's complete Agent Host Protocol server through the
|
|
43
|
+
same runtime used by the SDK:
|
|
44
|
+
|
|
45
|
+
```typescript
|
|
46
|
+
import { CopilotClient } from "@github/copilot-sdk";
|
|
47
|
+
|
|
48
|
+
await using client = new CopilotClient();
|
|
49
|
+
await client.start();
|
|
50
|
+
const host = await client.startAhpHost({
|
|
51
|
+
localServer: {},
|
|
52
|
+
onExit: (exit) => {
|
|
53
|
+
if (exit.error) console.error(exit.error);
|
|
54
|
+
},
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
// Connect an AHP client using host.url and host.token.
|
|
58
|
+
// Treat host.token as a secret; do not log it.
|
|
59
|
+
|
|
60
|
+
// Existing SDK sessions and AHP sessions share this runtime.
|
|
61
|
+
// Keep client alive while the listener is needed. Leaving this scope disposes
|
|
62
|
+
// client, and the runtime stops its listener without a separate host.dispose().
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
The runtime hosts the complete AHP server in-process; the SDK does not launch a
|
|
66
|
+
second runtime or relay the host's traffic. The host has its own SDK connection
|
|
67
|
+
and belongs to the client connection that started it. Explicit disposal,
|
|
68
|
+
connection loss, and runtime shutdown stop the listener and its hosting task
|
|
69
|
+
without deleting underlying sessions. Reconnecting does not reclaim a host.
|
|
70
|
+
The optional `onExit` callback reports exits at most once. If the owner connection
|
|
71
|
+
is lost, it reports that loss rather than claiming that listener cleanup was
|
|
72
|
+
acknowledged.
|
|
73
|
+
|
|
74
|
+
`host.pid` is absent for in-process listeners. The optional field is retained
|
|
75
|
+
for separate host process IDs returned by legacy runtimes, never the runtime PID.
|
|
76
|
+
Use `dispose()` to stop the listener. `reason: "exited"` reports hosting-task
|
|
77
|
+
failure, not runtime process death, and `exitCode` is absent. Hosting no longer
|
|
78
|
+
provides process isolation from the runtime.
|
|
79
|
+
|
|
80
|
+
Select at least one transport explicitly: `localServer: {}` enables the local
|
|
81
|
+
listener, `githubEnvironment: { name: "My app", computeId: "stable-installation-id" }`
|
|
82
|
+
registers a Mission Control environment and enables remote WPS connections, and
|
|
83
|
+
both enables both transports. GitHub-only hosting opens no local listener;
|
|
84
|
+
`host.url` and `host.token` are undefined, while `host.environmentId` identifies
|
|
85
|
+
the environment. The application supplies a stable compute ID and configures
|
|
86
|
+
transports only at startup. Dispose and recreate the host to change them.
|
|
87
|
+
|
|
88
|
+
The runtime validates options inside `localServer` and applies their defaults:
|
|
89
|
+
|
|
90
|
+
- `hostname` defaults to `127.0.0.1`. Set it explicitly to request a non-loopback
|
|
91
|
+
listener, such as `hostname: "0.0.0.0"`, and restrict network access appropriately.
|
|
92
|
+
- `port` defaults to `0`, which selects an available port.
|
|
93
|
+
- `requireConnectionToken` defaults to `true`. The runtime generates a random
|
|
94
|
+
token unless you supply a nonempty `token`.
|
|
95
|
+
- Set `requireConnectionToken: false` to disable token authentication;
|
|
96
|
+
`host.token` is then undefined. A supplied `token` cannot be combined with
|
|
97
|
+
`requireConnectionToken: false`.
|
|
98
|
+
|
|
99
|
+
The listener follows the owning client's lifetime. Call `await host.dispose()`
|
|
100
|
+
only when you want to stop it earlier; `await using host` also supports a shorter
|
|
101
|
+
scope. Each disposal call forwards to the runtime, which owns idempotent cleanup.
|
|
102
|
+
The AHP transport remains owned by the host.
|
|
103
|
+
|
|
104
|
+
### Application-owned sessions
|
|
105
|
+
|
|
106
|
+
Supply `createSession` to materialize fresh AHP sessions in your application:
|
|
107
|
+
|
|
108
|
+
```typescript
|
|
109
|
+
import { approveAll, CopilotClient, defineTool } from "@github/copilot-sdk";
|
|
110
|
+
|
|
111
|
+
await using client = new CopilotClient();
|
|
112
|
+
await using host = await client.startAhpHost({
|
|
113
|
+
localServer: {},
|
|
114
|
+
createSession: ({ config, signal }) => {
|
|
115
|
+
signal.throwIfAborted();
|
|
116
|
+
return client.createSession({
|
|
117
|
+
...config,
|
|
118
|
+
onPermissionRequest: approveAll,
|
|
119
|
+
systemMessage: { mode: "append", content: "Use the app's greeting tool." },
|
|
120
|
+
tools: [defineTool("greeting", {
|
|
121
|
+
description: "Get the application's greeting",
|
|
122
|
+
parameters: { type: "object", properties: {} },
|
|
123
|
+
handler: () => "Hello from the application!",
|
|
124
|
+
})],
|
|
125
|
+
});
|
|
126
|
+
},
|
|
127
|
+
onSessionReleased: async (originalSession) => {
|
|
128
|
+
// Optional: the app decides whether to disconnect, keep, or destroy it.
|
|
129
|
+
await originalSession.disconnect();
|
|
130
|
+
},
|
|
131
|
+
});
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
Preserve the supplied `config`, including its fresh session identity, workspace,
|
|
135
|
+
and selected host settings. Add your prompt and tools where the host has not
|
|
136
|
+
explicitly selected those settings; conflicting settings fail rather than
|
|
137
|
+
silently advertising configuration that was not applied. Return a normal session
|
|
138
|
+
created by this same client. The creation factory does not adopt an arbitrary existing
|
|
139
|
+
session or expose unrelated application sessions in the AHP catalog.
|
|
140
|
+
|
|
141
|
+
Only the session ID returns through ordinary SDK RPC. The host attaches to **that same
|
|
142
|
+
resident session**, adding its own callback/tool registrations without replacing
|
|
143
|
+
the application's prompt, tool filters, hooks, or tools. Application tool functions
|
|
144
|
+
continue running in the app while their results stream through the existing AHP
|
|
145
|
+
projector. No second application connection or function serialization is involved.
|
|
146
|
+
|
|
147
|
+
The SDK retains the original returned object until participation ends and invokes
|
|
148
|
+
`onSessionReleased` at most once per handoff, including attach failure, hosting-task exit,
|
|
149
|
+
and owner disconnection. The SDK never automatically disconnects or destroys the
|
|
150
|
+
app object. The creation callback receives an abort signal; materialization
|
|
151
|
+
is bounded to 30 seconds and cancellation also releases objects returned late.
|
|
152
|
+
Graceful host disposal waits for AHP detach before reporting release. Omitting
|
|
153
|
+
`createSession` preserves copilotd-owned creation.
|
|
154
|
+
|
|
155
|
+
To restore durable application-owned sessions, also supply `resumeSession`.
|
|
156
|
+
It receives `{ sessionId, config, signal }` (`AhpSessionResumeRequest`).
|
|
157
|
+
Return the object from this client's `resumeSession(sessionId, { ...config,
|
|
158
|
+
onPermissionRequest, ... })`, restoring your tools, hooks, and handlers.
|
|
159
|
+
Alternatively, return a retained original session from this client when it
|
|
160
|
+
still matches the requested identity and workspace.
|
|
161
|
+
Only catalog entries marked as application-owned invoke this callback.
|
|
162
|
+
If the callback is missing, restoring such an entry fails instead of falling
|
|
163
|
+
back to host-owned creation.
|
|
164
|
+
Published resident sessions attach directly, without invoking it or replacing
|
|
165
|
+
their current registrations. Resumed sessions follow the same original-object
|
|
166
|
+
retention, cancellation, late-result release, and `onSessionReleased` rules.
|
|
167
|
+
|
|
168
|
+
The `copilotd-hosting` library runs inside the runtime provider.
|
|
169
|
+
Development integrations require a
|
|
170
|
+
source-built launcher and provider (`COPILOT_RUNTIME_PROVIDER_LIB`). Older
|
|
171
|
+
runtimes without these RPC operations cannot start a host.
|
|
172
|
+
|
|
40
173
|
## Run the Sample
|
|
41
174
|
|
|
42
175
|
Try the interactive chat sample (from the repo root):
|
|
@@ -188,6 +321,9 @@ Start the CLI server and establish connection.
|
|
|
188
321
|
##### `stop(): Promise<Error[]>`
|
|
189
322
|
|
|
190
323
|
Stop the server and close all sessions. Returns a list of any errors encountered during cleanup.
|
|
324
|
+
For an owned stdio runtime, closes stdin and waits up to 10 seconds for host cleanup
|
|
325
|
+
(including telemetry export) and process exit before falling back to termination.
|
|
326
|
+
This graceful-exit timeout is separate from the shutdown RPC and post-termination wait.
|
|
191
327
|
|
|
192
328
|
##### `forceStop(): Promise<void>`
|
|
193
329
|
|
package/dist/cjs/cliVersion.js
CHANGED
|
@@ -22,7 +22,7 @@ __export(cliVersion_exports, {
|
|
|
22
22
|
COPILOT_CLI_VERSION: () => COPILOT_CLI_VERSION
|
|
23
23
|
});
|
|
24
24
|
module.exports = __toCommonJS(cliVersion_exports);
|
|
25
|
-
const COPILOT_CLI_VERSION = "1.0.
|
|
25
|
+
const COPILOT_CLI_VERSION = "1.0.90";
|
|
26
26
|
const COPILOT_CLI_USE_NPM_PACKAGE = false;
|
|
27
27
|
// Annotate the CommonJS export names for ESM import in node:
|
|
28
28
|
0 && (module.exports = {
|
package/dist/cjs/client.js
CHANGED
|
@@ -40,6 +40,7 @@ var import_node = require("vscode-jsonrpc/node.js");
|
|
|
40
40
|
var import_rpc = require("./generated/rpc.js");
|
|
41
41
|
var import_sdkProtocolVersion = require("./sdkProtocolVersion.js");
|
|
42
42
|
var import_session = require("./session.js");
|
|
43
|
+
var import_host = require("./host.js");
|
|
43
44
|
var import_runtimeArtifacts = require("./runtimeArtifacts.js");
|
|
44
45
|
var import_cliVersion = require("./cliVersion.js");
|
|
45
46
|
var import_sessionFsProvider = require("./sessionFsProvider.js");
|
|
@@ -51,6 +52,57 @@ var import_toolSet = require("./toolSet.js");
|
|
|
51
52
|
var import_types = require("./types.js");
|
|
52
53
|
const MIN_PROTOCOL_VERSION = 3;
|
|
53
54
|
const RUNTIME_SHUTDOWN_TIMEOUT_MS = 1e4;
|
|
55
|
+
function createMessageConnection(reader, writer) {
|
|
56
|
+
let dispatch;
|
|
57
|
+
let finishDrain;
|
|
58
|
+
let drained;
|
|
59
|
+
let disposed = false;
|
|
60
|
+
const barrier = { jsonrpc: "2.0", method: "$/sdkDrain" };
|
|
61
|
+
const connection = (0, import_node.createMessageConnection)(
|
|
62
|
+
{
|
|
63
|
+
onError: reader.onError,
|
|
64
|
+
onClose: reader.onClose,
|
|
65
|
+
onPartialMessage: reader.onPartialMessage,
|
|
66
|
+
listen: (callback) => {
|
|
67
|
+
dispatch = callback;
|
|
68
|
+
return reader.listen(callback);
|
|
69
|
+
},
|
|
70
|
+
dispose: () => reader.dispose()
|
|
71
|
+
},
|
|
72
|
+
writer,
|
|
73
|
+
void 0,
|
|
74
|
+
{
|
|
75
|
+
messageStrategy: {
|
|
76
|
+
handleMessage: (message, next) => {
|
|
77
|
+
if (message === barrier) {
|
|
78
|
+
finishDrain?.();
|
|
79
|
+
} else {
|
|
80
|
+
next(message);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
);
|
|
86
|
+
connection.onDispose(() => {
|
|
87
|
+
disposed = true;
|
|
88
|
+
finishDrain?.();
|
|
89
|
+
});
|
|
90
|
+
return Object.assign(connection, {
|
|
91
|
+
drain: () => {
|
|
92
|
+
if (!drained) {
|
|
93
|
+
drained = new Promise((resolve2) => {
|
|
94
|
+
finishDrain = resolve2;
|
|
95
|
+
if (disposed || !dispatch) {
|
|
96
|
+
resolve2();
|
|
97
|
+
} else {
|
|
98
|
+
dispatch(barrier);
|
|
99
|
+
}
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
return drained;
|
|
103
|
+
}
|
|
104
|
+
});
|
|
105
|
+
}
|
|
54
106
|
async function withTimeout(promise, timeoutMs, message) {
|
|
55
107
|
let timeout;
|
|
56
108
|
try {
|
|
@@ -237,6 +289,9 @@ class CopilotClient {
|
|
|
237
289
|
/** Shared in-flight start; concurrent callers await it instead of spawning another CLI. */
|
|
238
290
|
startPromise = null;
|
|
239
291
|
sessions = /* @__PURE__ */ new Map();
|
|
292
|
+
hostExitCallbacks = /* @__PURE__ */ new Map();
|
|
293
|
+
hostSessionFactories = /* @__PURE__ */ new Map();
|
|
294
|
+
hostHandoffs = /* @__PURE__ */ new Map();
|
|
240
295
|
stderrBuffer = "";
|
|
241
296
|
// Captures CLI stderr for error messages
|
|
242
297
|
/** Resolved connection mode chosen in the constructor. */
|
|
@@ -528,6 +583,11 @@ class CopilotClient {
|
|
|
528
583
|
}
|
|
529
584
|
setupClientGlobalHandlers() {
|
|
530
585
|
const handlers = {};
|
|
586
|
+
handlers.host = {
|
|
587
|
+
exited: async (exit) => {
|
|
588
|
+
this.handleHostExit(exit);
|
|
589
|
+
}
|
|
590
|
+
};
|
|
531
591
|
handlers.extensionLaunchProvider = this.extensionLaunchProvider;
|
|
532
592
|
if (this.requestHandler) {
|
|
533
593
|
this.requestAdapter = (0, import_copilotRequestHandler.createCopilotRequestAdapter)(this.requestHandler, () => {
|
|
@@ -681,7 +741,8 @@ class CopilotClient {
|
|
|
681
741
|
* 1. Closes all active sessions (releases in-memory resources)
|
|
682
742
|
* 2. Requests runtime shutdown for SDK-owned CLI processes
|
|
683
743
|
* 3. Closes the JSON-RPC connection
|
|
684
|
-
* 4.
|
|
744
|
+
* 4. Signals EOF to an owned stdio process and waits for host cleanup, then
|
|
745
|
+
* terminates the process if it does not exit within the shutdown timeout
|
|
685
746
|
*
|
|
686
747
|
* Note: session data on disk is preserved, so sessions can be resumed later.
|
|
687
748
|
* To permanently remove session data before stopping, call
|
|
@@ -799,7 +860,23 @@ class CopilotClient {
|
|
|
799
860
|
}
|
|
800
861
|
if (this.cliProcess && !this.isExternalServer) {
|
|
801
862
|
const child = this.cliProcess;
|
|
802
|
-
this.
|
|
863
|
+
if (this.connectionConfig.kind === "stdio" && child.stdin && child.exitCode == null && child.signalCode == null) {
|
|
864
|
+
const gracefulExitStart = Date.now();
|
|
865
|
+
try {
|
|
866
|
+
child.stdin.end();
|
|
867
|
+
const exited = await waitForChildExit(child, RUNTIME_SHUTDOWN_TIMEOUT_MS);
|
|
868
|
+
this.logDebugTiming(
|
|
869
|
+
exited ? "CopilotClient.stop graceful stdio exit complete" : "CopilotClient.stop graceful stdio exit timed out; terminating child",
|
|
870
|
+
gracefulExitStart
|
|
871
|
+
);
|
|
872
|
+
} catch (error) {
|
|
873
|
+
errors.push(
|
|
874
|
+
new Error(
|
|
875
|
+
`Failed to close CLI stdin: ${error instanceof Error ? error.message : String(error)}`
|
|
876
|
+
)
|
|
877
|
+
);
|
|
878
|
+
}
|
|
879
|
+
}
|
|
803
880
|
try {
|
|
804
881
|
if (child.exitCode == null && child.signalCode == null) {
|
|
805
882
|
child.kill();
|
|
@@ -817,6 +894,10 @@ class CopilotClient {
|
|
|
817
894
|
`Failed to kill CLI process: ${error instanceof Error ? error.message : String(error)}`
|
|
818
895
|
)
|
|
819
896
|
);
|
|
897
|
+
} finally {
|
|
898
|
+
if (this.cliProcess === child) {
|
|
899
|
+
this.cliProcess = null;
|
|
900
|
+
}
|
|
820
901
|
}
|
|
821
902
|
}
|
|
822
903
|
if (this.ffiHost) {
|
|
@@ -840,6 +921,7 @@ class CopilotClient {
|
|
|
840
921
|
this.runtimePort = null;
|
|
841
922
|
this.stderrBuffer = "";
|
|
842
923
|
this.processExitPromise = null;
|
|
924
|
+
this.disconnectHosts();
|
|
843
925
|
return errors;
|
|
844
926
|
}
|
|
845
927
|
/**
|
|
@@ -884,6 +966,7 @@ class CopilotClient {
|
|
|
884
966
|
*/
|
|
885
967
|
async forceStop() {
|
|
886
968
|
this.forceStopping = true;
|
|
969
|
+
this.disconnectHosts();
|
|
887
970
|
for (const session of this.sessions.values()) {
|
|
888
971
|
session._markDisconnected();
|
|
889
972
|
}
|
|
@@ -1183,6 +1266,7 @@ class CopilotClient {
|
|
|
1183
1266
|
const response = await this.connection.sendRequest("session.create", {
|
|
1184
1267
|
...await (0, import_telemetry.getTraceContext)(this.onGetTraceContext),
|
|
1185
1268
|
model: config.model,
|
|
1269
|
+
allowedModels: config.allowedModels,
|
|
1186
1270
|
sessionId: localSessionId,
|
|
1187
1271
|
clientName: config.clientName,
|
|
1188
1272
|
reasoningEffort: config.reasoningEffort,
|
|
@@ -1313,6 +1397,11 @@ class CopilotClient {
|
|
|
1313
1397
|
}
|
|
1314
1398
|
throw e;
|
|
1315
1399
|
}
|
|
1400
|
+
for (const entry of this.hostHandoffs.values()) {
|
|
1401
|
+
if (entry.requestedSessionId === session.sessionId) {
|
|
1402
|
+
entry.configs.set(session, config);
|
|
1403
|
+
}
|
|
1404
|
+
}
|
|
1316
1405
|
return session;
|
|
1317
1406
|
}
|
|
1318
1407
|
/**
|
|
@@ -1419,6 +1508,7 @@ class CopilotClient {
|
|
|
1419
1508
|
}
|
|
1420
1509
|
try {
|
|
1421
1510
|
const response = await this.connection.sendRequest("session.resume", {
|
|
1511
|
+
allowedModels: config.allowedModels,
|
|
1422
1512
|
...await (0, import_telemetry.getTraceContext)(this.onGetTraceContext),
|
|
1423
1513
|
sessionId,
|
|
1424
1514
|
clientName: config.clientName,
|
|
@@ -1547,8 +1637,205 @@ class CopilotClient {
|
|
|
1547
1637
|
}
|
|
1548
1638
|
throw e;
|
|
1549
1639
|
}
|
|
1640
|
+
for (const entry of this.hostHandoffs.values()) {
|
|
1641
|
+
if (entry.requestedSessionId === session.sessionId) {
|
|
1642
|
+
entry.configs.set(session, { ...config, sessionId });
|
|
1643
|
+
}
|
|
1644
|
+
}
|
|
1550
1645
|
return session;
|
|
1551
1646
|
}
|
|
1647
|
+
/**
|
|
1648
|
+
* Start a complete AHP listener hosted in-process by this runtime.
|
|
1649
|
+
*
|
|
1650
|
+
* This connection owns the host. Disposing it or disconnecting the client
|
|
1651
|
+
* stops the listener without deleting sessions. The host uses a separate
|
|
1652
|
+
* SDK connection to the same runtime, not another runtime process.
|
|
1653
|
+
* Only one AHP host may own the catalog in an effective Copilot home
|
|
1654
|
+
* at a time. Other SDK clients and sessions remain usable in that home.
|
|
1655
|
+
* Select localServer, githubEnvironment, or both. An empty localServer
|
|
1656
|
+
* selects 127.0.0.1 on an available port. `onExit` runs at most once; owner disconnection cannot
|
|
1657
|
+
* acknowledge listener cleanup over the disconnected transport.
|
|
1658
|
+
*
|
|
1659
|
+
* @experimental
|
|
1660
|
+
*/
|
|
1661
|
+
async startAhpHost(options) {
|
|
1662
|
+
if (!options?.localServer && !options?.githubEnvironment) {
|
|
1663
|
+
throw new Error("At least one of localServer or githubEnvironment is required");
|
|
1664
|
+
}
|
|
1665
|
+
if (this.state !== "connected") {
|
|
1666
|
+
await this.start();
|
|
1667
|
+
}
|
|
1668
|
+
const rpc = this.rpc;
|
|
1669
|
+
const hostId = (0, import_node_crypto.randomUUID)();
|
|
1670
|
+
const {
|
|
1671
|
+
localServer,
|
|
1672
|
+
githubEnvironment,
|
|
1673
|
+
onExit,
|
|
1674
|
+
createSession,
|
|
1675
|
+
resumeSession,
|
|
1676
|
+
onSessionReleased
|
|
1677
|
+
} = options;
|
|
1678
|
+
if (createSession || resumeSession) {
|
|
1679
|
+
this.hostSessionFactories.set(hostId, {
|
|
1680
|
+
createSession,
|
|
1681
|
+
resumeSession,
|
|
1682
|
+
onSessionReleased
|
|
1683
|
+
});
|
|
1684
|
+
}
|
|
1685
|
+
if (onExit) {
|
|
1686
|
+
this.hostExitCallbacks.set(hostId, onExit);
|
|
1687
|
+
}
|
|
1688
|
+
try {
|
|
1689
|
+
const info = await rpc.host.start({
|
|
1690
|
+
hostId,
|
|
1691
|
+
localServer,
|
|
1692
|
+
githubEnvironment,
|
|
1693
|
+
sessionFactory: createSession ? true : void 0,
|
|
1694
|
+
resumeFactory: resumeSession ? true : void 0
|
|
1695
|
+
});
|
|
1696
|
+
return new import_host.AhpHost(
|
|
1697
|
+
info,
|
|
1698
|
+
async () => {
|
|
1699
|
+
await rpc.host.dispose({ hostId });
|
|
1700
|
+
},
|
|
1701
|
+
(sessionId) => rpc.host.publishSession({ hostId, sessionId })
|
|
1702
|
+
);
|
|
1703
|
+
} catch (error) {
|
|
1704
|
+
this.hostExitCallbacks.delete(hostId);
|
|
1705
|
+
this.releaseHostSessions(hostId);
|
|
1706
|
+
throw error;
|
|
1707
|
+
}
|
|
1708
|
+
}
|
|
1709
|
+
handleHostExit(exit) {
|
|
1710
|
+
this.releaseHostSessions(exit.hostId);
|
|
1711
|
+
const onExit = this.hostExitCallbacks.get(exit.hostId);
|
|
1712
|
+
if (onExit) {
|
|
1713
|
+
this.hostExitCallbacks.delete(exit.hostId);
|
|
1714
|
+
const reportError = (error) => {
|
|
1715
|
+
console.error("AHP host exit callback failed", { hostId: exit.hostId, error });
|
|
1716
|
+
};
|
|
1717
|
+
try {
|
|
1718
|
+
void Promise.resolve(onExit(exit)).catch(reportError);
|
|
1719
|
+
} catch (error) {
|
|
1720
|
+
reportError(error);
|
|
1721
|
+
}
|
|
1722
|
+
}
|
|
1723
|
+
}
|
|
1724
|
+
disconnectHosts() {
|
|
1725
|
+
for (const hostId of /* @__PURE__ */ new Set([
|
|
1726
|
+
...this.hostExitCallbacks.keys(),
|
|
1727
|
+
...this.hostSessionFactories.keys()
|
|
1728
|
+
])) {
|
|
1729
|
+
this.handleHostExit({
|
|
1730
|
+
hostId,
|
|
1731
|
+
reason: "ownerDisconnected",
|
|
1732
|
+
error: "Owner connection closed; runtime cleanup cannot be acknowledged on this connection."
|
|
1733
|
+
});
|
|
1734
|
+
}
|
|
1735
|
+
}
|
|
1736
|
+
releaseHostSessions(hostId) {
|
|
1737
|
+
this.hostSessionFactories.delete(hostId);
|
|
1738
|
+
for (const [handoffId, entry] of this.hostHandoffs) {
|
|
1739
|
+
if (entry.hostId === hostId) this.releaseHostSession(handoffId);
|
|
1740
|
+
}
|
|
1741
|
+
}
|
|
1742
|
+
releaseHostSession(handoffId) {
|
|
1743
|
+
const entry = this.hostHandoffs.get(handoffId);
|
|
1744
|
+
if (!entry) return;
|
|
1745
|
+
this.hostHandoffs.delete(handoffId);
|
|
1746
|
+
entry.released = true;
|
|
1747
|
+
entry.configs.clear();
|
|
1748
|
+
entry.controller.abort();
|
|
1749
|
+
if (entry.session) this.notifyHostSessionReleased(entry);
|
|
1750
|
+
}
|
|
1751
|
+
notifyHostSessionReleased(entry) {
|
|
1752
|
+
const { session, onReleased } = entry;
|
|
1753
|
+
entry.session = void 0;
|
|
1754
|
+
if (session && onReleased) {
|
|
1755
|
+
try {
|
|
1756
|
+
void Promise.resolve(onReleased(session)).catch(() => {
|
|
1757
|
+
console.error("AHP session release callback failed");
|
|
1758
|
+
});
|
|
1759
|
+
} catch {
|
|
1760
|
+
console.error("AHP session release callback failed");
|
|
1761
|
+
}
|
|
1762
|
+
}
|
|
1763
|
+
}
|
|
1764
|
+
async createHostSession(params) {
|
|
1765
|
+
const options = this.hostSessionFactories.get(params.hostId);
|
|
1766
|
+
if (!(params.resume ? options?.resumeSession : options?.createSession) || this.hostHandoffs.has(params.handoffId)) {
|
|
1767
|
+
throw new Error("AHP session factory is unavailable or handoff already exists");
|
|
1768
|
+
}
|
|
1769
|
+
const entry = {
|
|
1770
|
+
hostId: params.hostId,
|
|
1771
|
+
requestedSessionId: params.config.sessionId,
|
|
1772
|
+
controller: new AbortController(),
|
|
1773
|
+
configs: /* @__PURE__ */ new Map(),
|
|
1774
|
+
released: false,
|
|
1775
|
+
onReleased: options.onSessionReleased
|
|
1776
|
+
};
|
|
1777
|
+
this.hostHandoffs.set(params.handoffId, entry);
|
|
1778
|
+
const { configDir, ...config } = params.config;
|
|
1779
|
+
if (configDir !== void 0) config.configDirectory = configDir;
|
|
1780
|
+
const expected = structuredClone(config);
|
|
1781
|
+
const cancelled = new Promise((_resolve, reject) => {
|
|
1782
|
+
entry.controller.signal.addEventListener(
|
|
1783
|
+
"abort",
|
|
1784
|
+
() => {
|
|
1785
|
+
reject(new Error("AHP session handoff ended"));
|
|
1786
|
+
},
|
|
1787
|
+
{ once: true }
|
|
1788
|
+
);
|
|
1789
|
+
});
|
|
1790
|
+
const materialized = Promise.resolve().then(() => {
|
|
1791
|
+
if (params.resume) {
|
|
1792
|
+
const { sessionId, ...resumeConfig } = config;
|
|
1793
|
+
if (!sessionId) throw new Error("AHP resume requires sessionId");
|
|
1794
|
+
return options.resumeSession({
|
|
1795
|
+
sessionId,
|
|
1796
|
+
config: resumeConfig,
|
|
1797
|
+
signal: entry.controller.signal
|
|
1798
|
+
});
|
|
1799
|
+
}
|
|
1800
|
+
return options.createSession({
|
|
1801
|
+
config,
|
|
1802
|
+
signal: entry.controller.signal
|
|
1803
|
+
});
|
|
1804
|
+
}).then((session) => {
|
|
1805
|
+
entry.session = session;
|
|
1806
|
+
const actual = entry.configs.get(session);
|
|
1807
|
+
entry.configs.clear();
|
|
1808
|
+
if (entry.released) {
|
|
1809
|
+
this.notifyHostSessionReleased(entry);
|
|
1810
|
+
throw new Error("AHP session handoff ended");
|
|
1811
|
+
}
|
|
1812
|
+
if (this.sessions.get(session.sessionId) !== session || session.sessionId !== expected.sessionId) {
|
|
1813
|
+
throw new Error(
|
|
1814
|
+
"AHP callback must return the requested session from this client"
|
|
1815
|
+
);
|
|
1816
|
+
}
|
|
1817
|
+
const contains = (value, required) => {
|
|
1818
|
+
if (required && typeof required === "object" && !Array.isArray(required)) {
|
|
1819
|
+
return !!value && typeof value === "object" && Object.entries(required).every(
|
|
1820
|
+
([key, child]) => contains(value[key], child)
|
|
1821
|
+
);
|
|
1822
|
+
}
|
|
1823
|
+
return JSON.stringify(value) === JSON.stringify(required);
|
|
1824
|
+
};
|
|
1825
|
+
if ((!params.resume || actual !== void 0) && !contains(actual, expected)) {
|
|
1826
|
+
throw new Error(
|
|
1827
|
+
"AHP callback must preserve the supplied session configuration"
|
|
1828
|
+
);
|
|
1829
|
+
}
|
|
1830
|
+
return { sessionId: session.sessionId };
|
|
1831
|
+
});
|
|
1832
|
+
try {
|
|
1833
|
+
return await Promise.race([materialized, cancelled]);
|
|
1834
|
+
} catch (error) {
|
|
1835
|
+
this.releaseHostSession(params.handoffId);
|
|
1836
|
+
throw error;
|
|
1837
|
+
}
|
|
1838
|
+
}
|
|
1552
1839
|
/**
|
|
1553
1840
|
* Sends a ping request to the server to verify connectivity.
|
|
1554
1841
|
*
|
|
@@ -2088,8 +2375,13 @@ stderr: ${stderrOutput}`
|
|
|
2088
2375
|
case "inprocess":
|
|
2089
2376
|
return this.connectViaFfi();
|
|
2090
2377
|
case "tcp":
|
|
2091
|
-
case "uri":
|
|
2092
2378
|
return this.connectViaTcp();
|
|
2379
|
+
case "uri": {
|
|
2380
|
+
const { host, port } = this.parseCliUrl(this.connectionConfig.url);
|
|
2381
|
+
this.actualHost = host;
|
|
2382
|
+
this.runtimePort = port;
|
|
2383
|
+
return this.connectViaTcp();
|
|
2384
|
+
}
|
|
2093
2385
|
}
|
|
2094
2386
|
}
|
|
2095
2387
|
/** Starts the in-process FFI runtime with SDK-managed typed options. */
|
|
@@ -2148,7 +2440,7 @@ stderr: ${stderrOutput}`
|
|
|
2148
2440
|
throw new Error("In-process FFI runtime host not started");
|
|
2149
2441
|
}
|
|
2150
2442
|
this.messageWriter = new TeardownResilientStreamMessageWriter(this.ffiHost.sendStream);
|
|
2151
|
-
this.connection =
|
|
2443
|
+
this.connection = createMessageConnection(
|
|
2152
2444
|
new import_node.StreamMessageReader(this.ffiHost.receiveStream),
|
|
2153
2445
|
this.messageWriter
|
|
2154
2446
|
);
|
|
@@ -2202,7 +2494,7 @@ stderr: ${stderrOutput}` : ""}`
|
|
|
2202
2494
|
}
|
|
2203
2495
|
});
|
|
2204
2496
|
this.messageWriter = new TeardownResilientStreamMessageWriter(this.cliProcess.stdin);
|
|
2205
|
-
this.connection =
|
|
2497
|
+
this.connection = createMessageConnection(
|
|
2206
2498
|
new import_node.StreamMessageReader(this.cliProcess.stdout),
|
|
2207
2499
|
this.messageWriter
|
|
2208
2500
|
);
|
|
@@ -2217,7 +2509,7 @@ stderr: ${stderrOutput}` : ""}`
|
|
|
2217
2509
|
throw new Error("CLI child process was unexpectedly started in parent process mode");
|
|
2218
2510
|
}
|
|
2219
2511
|
this.messageWriter = new TeardownResilientStreamMessageWriter(process.stdout);
|
|
2220
|
-
this.connection =
|
|
2512
|
+
this.connection = createMessageConnection(
|
|
2221
2513
|
new import_node.StreamMessageReader(process.stdin),
|
|
2222
2514
|
this.messageWriter
|
|
2223
2515
|
);
|
|
@@ -2240,7 +2532,7 @@ stderr: ${stderrOutput}` : ""}`
|
|
|
2240
2532
|
this.socket.connect(this.runtimePort, this.actualHost, () => {
|
|
2241
2533
|
clearTimeout(connectionTimeout);
|
|
2242
2534
|
this.messageWriter = new TeardownResilientStreamMessageWriter(this.socket);
|
|
2243
|
-
this.connection =
|
|
2535
|
+
this.connection = createMessageConnection(
|
|
2244
2536
|
new import_node.StreamMessageReader(this.socket),
|
|
2245
2537
|
this.messageWriter
|
|
2246
2538
|
);
|
|
@@ -2297,6 +2589,18 @@ stderr: ${stderrOutput}` : ""}`
|
|
|
2297
2589
|
(params, token) => confirmation.confirm(params, token)
|
|
2298
2590
|
);
|
|
2299
2591
|
}
|
|
2592
|
+
this.connection.onRequest(
|
|
2593
|
+
"host.materializeSession",
|
|
2594
|
+
(params) => this.createHostSession(params)
|
|
2595
|
+
);
|
|
2596
|
+
this.connection.onNotification(
|
|
2597
|
+
"host.sessionReleased",
|
|
2598
|
+
(params) => {
|
|
2599
|
+
if (this.hostHandoffs.get(params.handoffId)?.hostId === params.hostId) {
|
|
2600
|
+
this.releaseHostSession(params.handoffId);
|
|
2601
|
+
}
|
|
2602
|
+
}
|
|
2603
|
+
);
|
|
2300
2604
|
this.connection.onRequest(
|
|
2301
2605
|
"hooks.invoke",
|
|
2302
2606
|
async (params) => {
|
|
@@ -2304,20 +2608,27 @@ stderr: ${stderrOutput}` : ""}`
|
|
|
2304
2608
|
}
|
|
2305
2609
|
);
|
|
2306
2610
|
const connection = this.connection;
|
|
2611
|
+
const messageWriter = this.messageWriter;
|
|
2307
2612
|
const markDisconnected = () => {
|
|
2308
2613
|
if (this.connection !== connection) {
|
|
2614
|
+
connection.dispose();
|
|
2309
2615
|
return;
|
|
2310
2616
|
}
|
|
2311
2617
|
this.connectionClosed = true;
|
|
2312
2618
|
this.state = "disconnected";
|
|
2619
|
+
this.disconnectHosts();
|
|
2313
2620
|
for (const session of this.sessions.values()) {
|
|
2314
2621
|
session._markDisconnected();
|
|
2315
2622
|
}
|
|
2316
2623
|
this.sessions.clear();
|
|
2317
2624
|
this.githubTokenProviders.clear();
|
|
2318
2625
|
this.requestAdapter?.cancelPending();
|
|
2626
|
+
connection.dispose();
|
|
2319
2627
|
};
|
|
2320
|
-
this.connection.onClose(
|
|
2628
|
+
this.connection.onClose(() => {
|
|
2629
|
+
if (messageWriter) messageWriter.suppressWriteErrors = true;
|
|
2630
|
+
void connection.drain().then(markDisconnected);
|
|
2631
|
+
});
|
|
2321
2632
|
this.connection.onError(() => {
|
|
2322
2633
|
if (this.connection === connection) {
|
|
2323
2634
|
this.state = "disconnected";
|