@chopin-ai/harness-pi 0.0.0-stage → 0.2.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.
Files changed (51) hide show
  1. package/LICENSE +202 -0
  2. package/dist/acp.d.ts +42 -0
  3. package/dist/acp.js +122 -0
  4. package/dist/acp.js.map +1 -0
  5. package/dist/auth.d.ts +25 -0
  6. package/dist/auth.js +56 -0
  7. package/dist/auth.js.map +1 -0
  8. package/dist/backoff.d.ts +5 -0
  9. package/dist/backoff.js +13 -0
  10. package/dist/backoff.js.map +1 -0
  11. package/dist/bridge.d.ts +11 -0
  12. package/dist/bridge.js +259 -0
  13. package/dist/bridge.js.map +1 -0
  14. package/dist/http.d.ts +7 -0
  15. package/dist/http.js +73 -0
  16. package/dist/http.js.map +1 -0
  17. package/dist/index.d.ts +17 -0
  18. package/dist/index.js +11 -0
  19. package/dist/index.js.map +1 -0
  20. package/dist/layer.d.ts +8 -0
  21. package/dist/layer.js +84 -0
  22. package/dist/layer.js.map +1 -0
  23. package/dist/mcp.d.ts +24 -0
  24. package/dist/mcp.js +126 -0
  25. package/dist/mcp.js.map +1 -0
  26. package/dist/mode.d.ts +11 -0
  27. package/dist/mode.js +33 -0
  28. package/dist/mode.js.map +1 -0
  29. package/dist/pages.d.ts +8 -0
  30. package/dist/pages.js +12 -0
  31. package/dist/pages.js.map +1 -0
  32. package/dist/project-context.d.ts +18 -0
  33. package/dist/project-context.js +77 -0
  34. package/dist/project-context.js.map +1 -0
  35. package/dist/protocol.d.ts +247 -0
  36. package/dist/protocol.js +327 -0
  37. package/dist/protocol.js.map +1 -0
  38. package/dist/server.d.ts +25 -0
  39. package/dist/server.js +349 -0
  40. package/dist/server.js.map +1 -0
  41. package/dist/service.d.ts +96 -0
  42. package/dist/service.js +493 -0
  43. package/dist/service.js.map +1 -0
  44. package/dist/shell.d.ts +31 -0
  45. package/dist/shell.js +57 -0
  46. package/dist/shell.js.map +1 -0
  47. package/dist/submissions.d.ts +22 -0
  48. package/dist/submissions.js +33 -0
  49. package/dist/submissions.js.map +1 -0
  50. package/package.json +48 -3
  51. package/README.md +0 -3
package/dist/bridge.js ADDED
@@ -0,0 +1,259 @@
1
+ import { AgentSideConnection, PROTOCOL_VERSION, RequestError } from "@agentclientprotocol/sdk";
2
+ import { setTimeout as sleep } from "node:timers/promises";
3
+ import { entryUpdates, runInputs, stopReason, unansweredText } from "./acp.js";
4
+ import { call, CALL_TIMEOUT_MS, CallTimeout, heldList, randomId, startConversation, within, } from "./protocol.js";
5
+ import { lost as lostOutcome } from "./submissions.js";
6
+ const AWAY = "the service stopped answering";
7
+ const RESEND_WAIT_MS = 250;
8
+ /** A request the service could not answer, however that came to be known. */
9
+ function lostService(detail = AWAY) {
10
+ return RequestError.internalError({ reason: "lost", detail }, `the agent did not answer: ${unansweredText({ reason: "lost", detail })}`);
11
+ }
12
+ /**
13
+ * Speaks ACP on `stream` for the agent behind `client`, a host connected to the agent service, so any ACP client
14
+ * can drive it. One ACP session is one conversation.
15
+ */
16
+ export function serveAcp(stream, client, options = {}) {
17
+ let bridge;
18
+ const lostAfterMs = options.lostAfterMs ?? CALL_TIMEOUT_MS;
19
+ const connection = new AgentSideConnection((opened) => (bridge = new Bridge(opened, client, lostAfterMs)), stream);
20
+ void connection.closed.finally(() => bridge?.release());
21
+ return connection;
22
+ }
23
+ class Bridge {
24
+ #connection;
25
+ #client;
26
+ #lostAfterMs;
27
+ #sessions = new Map();
28
+ /** Aborted once the client is gone. */
29
+ #gone = new AbortController();
30
+ #list;
31
+ constructor(connection, client, lostAfterMs) {
32
+ this.#connection = connection;
33
+ this.#client = client;
34
+ this.#lostAfterMs = lostAfterMs;
35
+ this.#list = heldList(client, { timeoutMs: lostAfterMs, signal: this.#gone.signal });
36
+ }
37
+ /** Lets go of every session's conversation, and ends the prompts still waiting, once the client is gone. */
38
+ async release() {
39
+ this.#gone.abort();
40
+ this.#list.release();
41
+ const sessions = [...this.#sessions.values()];
42
+ this.#sessions.clear();
43
+ await Promise.all(sessions.map(async (session) => (await session.catch(() => undefined))?.release()));
44
+ }
45
+ async initialize() {
46
+ return { protocolVersion: PROTOCOL_VERSION, agentCapabilities: {}, authMethods: [] };
47
+ }
48
+ async authenticate() {
49
+ return {};
50
+ }
51
+ async newSession() {
52
+ const { id, conversation } = await this.#reach((options) => startConversation(this.#client, this.#list.get, options));
53
+ if (this.#gone.signal.aborted) {
54
+ conversation.release();
55
+ throw RequestError.internalError(undefined, "the client has gone");
56
+ }
57
+ this.#sessions.set(String(id), Promise.resolve(conversation));
58
+ return { sessionId: String(id) };
59
+ }
60
+ async prompt({ sessionId, prompt }) {
61
+ const conversation = await this.#session(sessionId);
62
+ // Text and resource links are what every ACP agent takes; a link reaches the model as its name and its address.
63
+ const text = prompt.flatMap((block) => {
64
+ if (block.type === "text")
65
+ return [block.text];
66
+ if (block.type === "resource_link")
67
+ return [`${block.name} <${block.uri}>`];
68
+ return [];
69
+ }).join("\n");
70
+ if (text.trim() === "")
71
+ throw RequestError.invalidParams(undefined, "the prompt has no text or link");
72
+ const sent = new Set(view(conversation).entries.map((entry) => entry.id));
73
+ const answers = [];
74
+ let forwarding = Promise.resolve();
75
+ let undelivered;
76
+ // Committed entries go out in order, each once; the client already has its own prompt.
77
+ const forward = () => {
78
+ forwarding = forwarding.then(async () => {
79
+ if (conversation.status !== "live")
80
+ return;
81
+ for (const entry of view(conversation).entries.filter((e) => !sent.has(e.id))) {
82
+ sent.add(entry.id);
83
+ answers.push(...(entry.model ?? []).filter((m) => m.role === "assistant"));
84
+ for (const update of entryUpdates(entry)) {
85
+ if (update.sessionUpdate !== "user_message_chunk")
86
+ await this.#connection.sessionUpdate({ sessionId, update });
87
+ }
88
+ }
89
+ }).catch((error) => {
90
+ undelivered ??= error;
91
+ });
92
+ };
93
+ const submission = await this.#send(sessionId, conversation, text);
94
+ const outcome = await this.#follow(sessionId, conversation, submission, forward);
95
+ if (outcome.status === "gone")
96
+ return { stopReason: "cancelled" };
97
+ if (outcome.status === "ended")
98
+ throw RequestError.resourceNotFound(sessionId);
99
+ forward();
100
+ await forwarding;
101
+ if (undelivered !== undefined)
102
+ throw undelivered;
103
+ if (outcome.status === "unanswered") {
104
+ if (outcome.reason === "aborted")
105
+ return { stopReason: "cancelled" };
106
+ if (outcome.reason === "lost")
107
+ throw lostService(outcome.detail);
108
+ const why = unansweredText({ reason: outcome.reason ?? "unknown", detail: outcome.detail });
109
+ throw RequestError.internalError({ reason: outcome.reason, detail: outcome.detail }, `the agent did not answer: ${why}`);
110
+ }
111
+ return { stopReason: stopReason(answers.at(-1)) };
112
+ }
113
+ /**
114
+ * Submits `text` under a request of its own, so it can be sent again when the link fails mid-call: the service
115
+ * admits it once, whether or not the first try reached it.
116
+ */
117
+ async #send(sessionId, conversation, text) {
118
+ const request = randomId();
119
+ // One bound for every try, so a send waits no longer than any other request.
120
+ return this.#reach((options) => within(options, async (signal) => {
121
+ for (;;) {
122
+ // Only a call whose link wavered while it ran is sent again; one on a steady link failed for what it is.
123
+ let wavered = false;
124
+ const stop = conversation.onStatus(() => void (wavered = true));
125
+ try {
126
+ return await this.#client.root((ctx) => conversation.submit({ text, request }, ctx), { signal });
127
+ }
128
+ catch (error) {
129
+ // The link's own news may trail the call's failure by a turn of the event loop.
130
+ await sleep(0);
131
+ if (conversation.status === "gone" || conversation.status === "released") {
132
+ this.#forget(sessionId, conversation);
133
+ throw RequestError.resourceNotFound(sessionId);
134
+ }
135
+ if (!wavered && conversation.status === "live")
136
+ throw error;
137
+ await sleep(RESEND_WAIT_MS, undefined, { signal });
138
+ }
139
+ finally {
140
+ stop();
141
+ }
142
+ }
143
+ }));
144
+ }
145
+ async cancel({ sessionId }) {
146
+ const conversation = await this.#session(sessionId);
147
+ await this.#call((ctx) => conversation.abort({}, ctx));
148
+ }
149
+ /**
150
+ * How `submission` ends: once it has settled and the view no longer shows it running, so the last answer is in
151
+ * the view. A reconnect brings the object's state back as the store has it; a service that stays away, or a
152
+ * conversation that ended, takes the session with it.
153
+ */
154
+ #follow(sessionId, conversation, submission, changed) {
155
+ const stops = [];
156
+ let ended = false;
157
+ return new Promise((resolve) => {
158
+ const settle = (outcome) => {
159
+ if (ended)
160
+ return;
161
+ ended = true;
162
+ resolve(outcome);
163
+ };
164
+ const lost = (detail) => settle(lostOutcome(detail));
165
+ const check = () => {
166
+ if (ended || conversation.status !== "live")
167
+ return;
168
+ changed();
169
+ const current = conversation.submissions.value[String(submission)];
170
+ if (current !== undefined && current.status !== "pending" && !runInputs(view(conversation)).includes(submission))
171
+ settle(current);
172
+ };
173
+ let away;
174
+ const followed = (status) => {
175
+ if (ended)
176
+ return;
177
+ clearTimeout(away);
178
+ if (status === "live") {
179
+ check();
180
+ }
181
+ else if (status === "loading") {
182
+ away = setTimeout(() => {
183
+ lost(AWAY);
184
+ this.#forget(sessionId, conversation);
185
+ }, this.#lostAfterMs);
186
+ }
187
+ else {
188
+ settle({ status: "ended" });
189
+ this.#forget(sessionId, conversation);
190
+ }
191
+ };
192
+ // The agent keeps working for a client that has gone, but nobody waits for it here any longer.
193
+ const gone = () => settle({ status: "gone" });
194
+ this.#gone.signal.addEventListener("abort", gone, { once: true });
195
+ stops.push(() => clearTimeout(away), () => this.#gone.signal.removeEventListener("abort", gone));
196
+ // Subscribing delivers the current value at once, so the outcome may already be known here.
197
+ stops.push(conversation.view.subscribe(check), conversation.submissions.subscribe(check), conversation.onStatus(followed));
198
+ // Status listeners hear only of changes; one that already happened is taken as it stands.
199
+ if (conversation.status !== "live")
200
+ followed(conversation.status);
201
+ if (this.#gone.signal.aborted)
202
+ gone();
203
+ }).finally(() => stops.forEach((stop) => stop()));
204
+ }
205
+ /** A held conversation, once it is live: waiting out a reconnect, but no longer than the service may stay away. */
206
+ async #session(sessionId) {
207
+ const session = this.#sessions.get(sessionId);
208
+ if (session === undefined)
209
+ throw RequestError.resourceNotFound(sessionId);
210
+ const conversation = await session;
211
+ await this.#reach((options) => within(options, (signal) => this.#live(sessionId, conversation, signal)));
212
+ return conversation;
213
+ }
214
+ /** Resolves once the session's conversation is live; one that ended takes the session with it. */
215
+ #live(sessionId, conversation, signal) {
216
+ return new Promise((resolve, reject) => {
217
+ const settled = (status) => {
218
+ if (status === "loading")
219
+ return;
220
+ stop();
221
+ signal.removeEventListener("abort", aborted);
222
+ if (status === "live")
223
+ return resolve();
224
+ this.#forget(sessionId, conversation);
225
+ reject(RequestError.resourceNotFound(sessionId));
226
+ };
227
+ const aborted = () => {
228
+ stop();
229
+ reject(signal.reason);
230
+ };
231
+ const stop = conversation.onStatus(settled);
232
+ if (signal.aborted)
233
+ return aborted();
234
+ signal.addEventListener("abort", aborted, { once: true });
235
+ settled(conversation.status);
236
+ });
237
+ }
238
+ #forget(sessionId, conversation) {
239
+ this.#sessions.delete(sessionId);
240
+ conversation.release();
241
+ }
242
+ /** A method call on an object the bridge holds, through any reconnect, as long as the service may stay away. */
243
+ #call(work) {
244
+ return this.#reach((options) => call(this.#client, work, options));
245
+ }
246
+ /** Any wait on the service, for as long as it may stay away and the client is here. */
247
+ async #reach(work) {
248
+ try {
249
+ return await work({ timeoutMs: this.#lostAfterMs, signal: this.#gone.signal });
250
+ }
251
+ catch (error) {
252
+ throw error instanceof CallTimeout || this.#gone.signal.aborted ? lostService() : error;
253
+ }
254
+ }
255
+ }
256
+ function view(conversation) {
257
+ return conversation.view.value;
258
+ }
259
+ //# sourceMappingURL=bridge.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"bridge.js","sourceRoot":"","sources":["../src/bridge.ts"],"names":[],"mappings":"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"}
package/dist/http.d.ts ADDED
@@ -0,0 +1,7 @@
1
+ import type { IncomingMessage, ServerResponse } from "node:http";
2
+ import { type Authenticate } from "./auth.js";
3
+ /**
4
+ * The HTTP side of the service: it turns a credential presented in the address into a cookie, then serves the web
5
+ * app from `root`, if any. Data only ever travels over the authenticated WebSocket.
6
+ */
7
+ export declare function httpHandler(authenticate: Authenticate, root: string | undefined): (request: IncomingMessage, response: ServerResponse) => Promise<void>;
package/dist/http.js ADDED
@@ -0,0 +1,73 @@
1
+ import { open, stat } from "node:fs/promises";
2
+ import { extname, join, relative, resolve, sep } from "node:path";
3
+ import { pipeline } from "node:stream";
4
+ import { credentialCookie } from "./auth.js";
5
+ const TYPES = {
6
+ ".html": "text/html; charset=utf-8",
7
+ ".js": "text/javascript; charset=utf-8",
8
+ ".css": "text/css; charset=utf-8",
9
+ ".json": "application/json",
10
+ ".svg": "image/svg+xml",
11
+ ".png": "image/png",
12
+ ".ico": "image/x-icon",
13
+ ".woff2": "font/woff2",
14
+ };
15
+ /**
16
+ * The HTTP side of the service: it turns a credential presented in the address into a cookie, then serves the web
17
+ * app from `root`, if any. Data only ever travels over the authenticated WebSocket.
18
+ */
19
+ export function httpHandler(authenticate, root) {
20
+ return async (request, response) => {
21
+ const url = new URL(request.url ?? "/", "http://localhost");
22
+ const token = url.searchParams.get("token");
23
+ if (token !== null) {
24
+ if ((await authenticate(token)) === undefined)
25
+ return void response.writeHead(401).end("unauthorized");
26
+ url.searchParams.delete("token");
27
+ // The credential moves into a cookie, so it stays neither in the address bar nor in the history. A relative
28
+ // redirect never leaves this service, and keeps any path a proxy in front adds.
29
+ response.writeHead(303, {
30
+ location: `./${url.pathname.slice(url.pathname.lastIndexOf("/") + 1)}${url.search}`,
31
+ // With no Path, the browser keeps the cookie for the page's own directory, so services a proxy serves under
32
+ // different paths of one host each keep their own.
33
+ "set-cookie": `${credentialCookie(request)}=${encodeURIComponent(token)}; HttpOnly; SameSite=Strict${overTls(request) ? "; Secure" : ""}`,
34
+ });
35
+ return void response.end();
36
+ }
37
+ const pathname = decoded(url.pathname);
38
+ const file = root === undefined || pathname === undefined ? undefined : await asset(resolve(root), pathname);
39
+ if (file === undefined)
40
+ return void response.writeHead(404).end("not found");
41
+ const handle = await open(file).catch(() => undefined);
42
+ if (handle === undefined)
43
+ return void response.writeHead(404).end("not found");
44
+ response.writeHead(200, { "content-type": TYPES[extname(file)] ?? "application/octet-stream" });
45
+ // A client that leaves mid-download closes the file with it.
46
+ pipeline(handle.createReadStream(), response, () => { });
47
+ };
48
+ }
49
+ /**
50
+ * The file a path names inside `root`, and the app's page for the root itself; the page loads its files relative to
51
+ * where it is, so it is served nowhere else.
52
+ */
53
+ async function asset(root, pathname) {
54
+ const path = join(root, pathname === "/" ? "index.html" : pathname);
55
+ if (relative(root, path).split(sep)[0] === "..")
56
+ return undefined;
57
+ return (await stat(path).catch(() => undefined))?.isFile() === true ? path : undefined;
58
+ }
59
+ function decoded(pathname) {
60
+ try {
61
+ return decodeURIComponent(pathname);
62
+ }
63
+ catch {
64
+ return undefined;
65
+ }
66
+ }
67
+ /** Whether the browser reached the service over TLS, itself or through a proxy that says so. */
68
+ function overTls(request) {
69
+ const forwarded = request.headers["x-forwarded-proto"];
70
+ const proto = (Array.isArray(forwarded) ? forwarded[0] : forwarded)?.split(",")[0]?.trim();
71
+ return proto === "https" || request.socket.encrypted === true;
72
+ }
73
+ //# sourceMappingURL=http.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"http.js","sourceRoot":"","sources":["../src/http.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAE,IAAI,EAAE,MAAM,kBAAkB,CAAC;AAE9C,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,QAAQ,EAAE,OAAO,EAAE,GAAG,EAAE,MAAM,WAAW,CAAC;AAClE,OAAO,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAC;AAEvC,OAAO,EAAE,gBAAgB,EAAqB,MAAM,WAAW,CAAC;AAEhE,MAAM,KAAK,GAAqC;IAC9C,OAAO,EAAE,0BAA0B;IACnC,KAAK,EAAE,gCAAgC;IACvC,MAAM,EAAE,yBAAyB;IACjC,OAAO,EAAE,kBAAkB;IAC3B,MAAM,EAAE,eAAe;IACvB,MAAM,EAAE,WAAW;IACnB,MAAM,EAAE,cAAc;IACtB,QAAQ,EAAE,YAAY;CACvB,CAAC;AAEF;;;GAGG;AACH,MAAM,UAAU,WAAW,CAAC,YAA0B,EAAE,IAAwB;IAC9E,OAAO,KAAK,EAAE,OAAwB,EAAE,QAAwB,EAAiB,EAAE;QACjF,MAAM,GAAG,GAAG,IAAI,GAAG,CAAC,OAAO,CAAC,GAAG,IAAI,GAAG,EAAE,kBAAkB,CAAC,CAAC;QAC5D,MAAM,KAAK,GAAG,GAAG,CAAC,YAAY,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC;QAC5C,IAAI,KAAK,KAAK,IAAI,EAAE,CAAC;YACnB,IAAI,CAAC,MAAM,YAAY,CAAC,KAAK,CAAC,CAAC,KAAK,SAAS;gBAAE,OAAO,KAAK,QAAQ,CAAC,SAAS,CAAC,GAAG,CAAC,CAAC,GAAG,CAAC,cAAc,CAAC,CAAC;YACvG,GAAG,CAAC,YAAY,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC;YACjC,4GAA4G;YAC5G,gFAAgF;YAChF,QAAQ,CAAC,SAAS,CAAC,GAAG,EAAE;gBACtB,QAAQ,EAAE,KAAK,GAAG,CAAC,QAAQ,CAAC,KAAK,CAAC,GAAG,CAAC,QAAQ,CAAC,WAAW,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,GAAG,GAAG,CAAC,MAAM,EAAE;gBACnF,4GAA4G;gBAC5G,mDAAmD;gBACnD,YAAY,EAAE,GAAG,gBAAgB,CAAC,OAAO,CAAC,IAAI,kBAAkB,CAAC,KAAK,CAAC,8BAA8B,OAAO,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,UAAU,CAAC,CAAC,CAAC,EAAE,EAAE;aAC1I,CAAC,CAAC;YACH,OAAO,KAAK,QAAQ,CAAC,GAAG,EAAE,CAAC;QAC7B,CAAC;QACD,MAAM,QAAQ,GAAG,OAAO,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC;QACvC,MAAM,IAAI,GAAG,IAAI,KAAK,SAAS,IAAI,QAAQ,KAAK,SAAS,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,MAAM,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,EAAE,QAAQ,CAAC,CAAC;QAC7G,IAAI,IAAI,KAAK,SAAS;YAAE,OAAO,KAAK,QAAQ,CAAC,SAAS,CAAC,GAAG,CAAC,CAAC,GAAG,CAAC,WAAW,CAAC,CAAC;QAC7E,MAAM,MAAM,GAAG,MAAM,IAAI,CAAC,IAAI,CAAC,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,SAAS,CAAC,CAAC;QACvD,IAAI,MAAM,KAAK,SAAS;YAAE,OAAO,KAAK,QAAQ,CAAC,SAAS,CAAC,GAAG,CAAC,CAAC,GAAG,CAAC,WAAW,CAAC,CAAC;QAC/E,QAAQ,CAAC,SAAS,CAAC,GAAG,EAAE,EAAE,cAAc,EAAE,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,IAAI,0BAA0B,EAAE,CAAC,CAAC;QAChG,6DAA6D;QAC7D,QAAQ,CAAC,MAAM,CAAC,gBAAgB,EAAE,EAAE,QAAQ,EAAE,GAAG,EAAE,GAAE,CAAC,CAAC,CAAC;IAC1D,CAAC,CAAC;AACJ,CAAC;AAED;;;GAGG;AACH,KAAK,UAAU,KAAK,CAAC,IAAY,EAAE,QAAgB;IACjD,MAAM,IAAI,GAAG,IAAI,CAAC,IAAI,EAAE,QAAQ,KAAK,GAAG,CAAC,CAAC,CAAC,YAAY,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC;IACpE,IAAI,QAAQ,CAAC,IAAI,EAAE,IAAI,CAAC,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,KAAK,IAAI;QAAE,OAAO,SAAS,CAAC;IAClE,OAAO,CAAC,MAAM,IAAI,CAAC,IAAI,CAAC,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,SAAS,CAAC,CAAC,EAAE,MAAM,EAAE,KAAK,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,SAAS,CAAC;AACzF,CAAC;AAED,SAAS,OAAO,CAAC,QAAgB;IAC/B,IAAI,CAAC;QACH,OAAO,kBAAkB,CAAC,QAAQ,CAAC,CAAC;IACtC,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,SAAS,CAAC;IACnB,CAAC;AACH,CAAC;AAED,gGAAgG;AAChG,SAAS,OAAO,CAAC,OAAwB;IACvC,MAAM,SAAS,GAAG,OAAO,CAAC,OAAO,CAAC,mBAAmB,CAAC,CAAC;IACvD,MAAM,KAAK,GAAG,CAAC,KAAK,CAAC,OAAO,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC,EAAE,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,CAAC;IAC3F,OAAO,KAAK,KAAK,OAAO,IAAK,OAAO,CAAC,MAAoB,CAAC,SAAS,KAAK,IAAI,CAAC;AAC/E,CAAC"}
@@ -0,0 +1,17 @@
1
+ export { compactionFailure, entryTranscript, entryUpdates, inputText, inWrittenOrder, isCompacting, isRunning, liveUpdates, runInputs, toolText, userText, } from "./acp.js";
2
+ export { serveAcp } from "./bridge.js";
3
+ export type { BridgeOptions } from "./bridge.js";
4
+ export { layerExtension } from "./layer.js";
5
+ export { McpServers } from "./mcp.js";
6
+ export { AgentService, DAEMON_PATH } from "./service.js";
7
+ export type { AgentServiceOptions } from "./service.js";
8
+ export { integratorTokens, launchToken } from "./auth.js";
9
+ export type { Authenticate, IntegratorTokenOptions } from "./auth.js";
10
+ export { ModeDoc } from "./mode.js";
11
+ export type { Mode } from "./protocol.js";
12
+ export { createAgentHost, listen } from "./server.js";
13
+ export { contextFiles, projectContext } from "./project-context.js";
14
+ export type { ContextFile } from "./project-context.js";
15
+ export { ShellEntry, shellText } from "./shell.js";
16
+ export type { ShellRun } from "./shell.js";
17
+ export type { Listening, ListenOptions } from "./server.js";
package/dist/index.js ADDED
@@ -0,0 +1,11 @@
1
+ export { compactionFailure, entryTranscript, entryUpdates, inputText, inWrittenOrder, isCompacting, isRunning, liveUpdates, runInputs, toolText, userText, } from "./acp.js";
2
+ export { serveAcp } from "./bridge.js";
3
+ export { layerExtension } from "./layer.js";
4
+ export { McpServers } from "./mcp.js";
5
+ export { AgentService, DAEMON_PATH } from "./service.js";
6
+ export { integratorTokens, launchToken } from "./auth.js";
7
+ export { ModeDoc } from "./mode.js";
8
+ export { createAgentHost, listen } from "./server.js";
9
+ export { contextFiles, projectContext } from "./project-context.js";
10
+ export { ShellEntry, shellText } from "./shell.js";
11
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,iBAAiB,EACjB,eAAe,EACf,YAAY,EACZ,SAAS,EACT,cAAc,EACd,YAAY,EACZ,SAAS,EACT,WAAW,EACX,SAAS,EACT,QAAQ,EACR,QAAQ,GACT,MAAM,UAAU,CAAC;AAClB,OAAO,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAC;AAEvC,OAAO,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC;AAC5C,OAAO,EAAE,UAAU,EAAE,MAAM,UAAU,CAAC;AACtC,OAAO,EAAE,YAAY,EAAE,WAAW,EAAE,MAAM,cAAc,CAAC;AAEzD,OAAO,EAAE,gBAAgB,EAAE,WAAW,EAAE,MAAM,WAAW,CAAC;AAE1D,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAEpC,OAAO,EAAE,eAAe,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AACtD,OAAO,EAAE,YAAY,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AAEpE,OAAO,EAAE,UAAU,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC"}
@@ -0,0 +1,8 @@
1
+ import { type Extension } from "@earendil-works/pi-durable";
2
+ import { type Layer } from "@chopin-ai/core";
3
+ /**
4
+ * The portable layer as an extension. Skills and instructions are served from here, outside the workspace, so the
5
+ * agent can read them but never edit them; only evaluation changes the layer. A refused call names any required
6
+ * tool missing from `registered`, such as one whose MCP server did not answer.
7
+ */
8
+ export declare function layerExtension(layer: Layer, registered: ReadonlySet<string>): Extension;
package/dist/layer.js ADDED
@@ -0,0 +1,84 @@
1
+ import { readFile } from "node:fs/promises";
2
+ import { relative, resolve, sep } from "node:path";
3
+ import { Type } from "@earendil-works/pi-ai";
4
+ import { defineDoc, defineExtension, defineTool, hook, section, ToolTask, } from "@earendil-works/pi-durable";
5
+ import { gateRefusal, skillBody, unmetGates } from "@chopin-ai/core";
6
+ /** Tools that have completed successfully in a conversation; a fork sees them as of its fork point. */
7
+ const Succeeded = defineDoc({
8
+ kind: "chopin.succeeded",
9
+ version: 1,
10
+ scope: "conversation",
11
+ history: "rewindable",
12
+ fork: "asOf",
13
+ initial: () => ({ tools: [] }),
14
+ });
15
+ /**
16
+ * The portable layer as an extension. Skills and instructions are served from here, outside the workspace, so the
17
+ * agent can read them but never edit them; only evaluation changes the layer. A refused call names any required
18
+ * tool missing from `registered`, such as one whose MCP server did not answer.
19
+ */
20
+ export function layerExtension(layer, registered) {
21
+ const skills = new Map(layer.skills.map((skill) => [skill.name, skill]));
22
+ const required = [...new Set(layer.gates.flatMap((rule) => rule.requires))];
23
+ return defineExtension({
24
+ name: "chopin-layer",
25
+ sections: [
26
+ section("guidance", () => layer.instructions),
27
+ section("skills", () => (layer.skills.length === 0 ? undefined : skillIndex(layer.skills))),
28
+ ],
29
+ tools: layer.skills.length === 0 ? [] : [skillTool(skills)],
30
+ hooks: layer.gates.length === 0 ? [] : [hook(ToolTask, {
31
+ beforeTool: async (call, api, context) => {
32
+ const succeeded = (await api.snapshot(Succeeded, api.conversationId, context))?.tools ?? [];
33
+ const unmet = unmetGates(layer.gates, call.name, new Set(succeeded));
34
+ return unmet.length === 0 ? undefined : { block: gateRefusal(unmet, registered) };
35
+ },
36
+ })],
37
+ wraps: required.map(recordSuccess),
38
+ });
39
+ }
40
+ function skillIndex(skills) {
41
+ const lines = skills.map((skill) => `- ${skill.name}: ${skill.description}`);
42
+ return ["Skills; read one with the skill tool when its description applies:", ...lines].join("\n");
43
+ }
44
+ function skillTool(skills) {
45
+ return defineTool({
46
+ name: "skill",
47
+ description: "Read a skill's instructions, or a file the skill bundles.",
48
+ parameters: Type.Object({
49
+ name: Type.String({ description: "The skill's name" }),
50
+ file: Type.Optional(Type.String({ description: "A file inside the skill, relative to it; SKILL.md when omitted" })),
51
+ }),
52
+ execute: async ({ name, file }) => {
53
+ const skill = skills.get(name);
54
+ if (skill === undefined)
55
+ throw new Error(`no skill named ${name}`);
56
+ const path = resolve(skill.directory, file ?? "SKILL.md");
57
+ if (relative(skill.directory, path).split(sep)[0] === ".." || path === skill.directory) {
58
+ throw new Error(`${file} is outside the skill`);
59
+ }
60
+ const text = await readFile(path, "utf8");
61
+ return { output: [{ type: "text", text: path === resolve(skill.directory, "SKILL.md") ? skillBody(text) : text }] };
62
+ },
63
+ });
64
+ }
65
+ function recordSuccess(tool) {
66
+ return {
67
+ tool,
68
+ wrap: (registration) => ({
69
+ ...registration,
70
+ execute: async (args, api, context) => {
71
+ const result = await registration.execute(args, api, context);
72
+ if (result.isError !== true) {
73
+ await api.commit(async (tx) => {
74
+ const doc = await tx.doc(Succeeded, api.conversationId);
75
+ if (!doc.tools.includes(tool))
76
+ doc.tools.push(tool);
77
+ }, context);
78
+ }
79
+ return result;
80
+ },
81
+ }),
82
+ };
83
+ }
84
+ //# sourceMappingURL=layer.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"layer.js","sourceRoot":"","sources":["../src/layer.ts"],"names":[],"mappings":"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"}
package/dist/mcp.d.ts ADDED
@@ -0,0 +1,24 @@
1
+ import { type Extension } from "@earendil-works/pi-durable";
2
+ import { type McpServer } from "@chopin-ai/core";
3
+ import type { Workspace } from "@chopin-ai/workspace";
4
+ /**
5
+ * One owner's MCP servers. A command server runs inside the owner's workspace, never on the service host: the
6
+ * layer may change, and what it starts must stay as confined as the agent itself.
7
+ */
8
+ export declare class McpServers {
9
+ #private;
10
+ /** Names of the offered tools that declare they change nothing. */
11
+ readonly readOnly: Set<string>;
12
+ /** Servers that could not be reached when their tools were offered. */
13
+ readonly unavailable: Set<string>;
14
+ constructor(servers: Readonly<Record<string, McpServer>>, workspace: Workspace);
15
+ /** Connects every server and offers its tools, one extension per server; one that cannot be reached is left out. */
16
+ connect(report: (error: Error) => void): Promise<Extension[]>;
17
+ /**
18
+ * Tries the unavailable servers again and offers the tools of those that answer now; a server in the workspace is
19
+ * tried only while the workspace runs, so trying never wakes one stopped for being idle.
20
+ */
21
+ retry(report: (error: Error) => void): Promise<Extension[]>;
22
+ /** Closes every client, and refuses to connect another, such as for a retry still under way. */
23
+ close(): Promise<void>;
24
+ }