@particle-academy/prism-acp 0.2.0 → 0.4.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/README.md +69 -0
- package/dist/acp/agent.d.ts +14 -0
- package/dist/acp/agent.js +84 -2
- package/dist/claude/session-store.d.ts +19 -0
- package/dist/claude/session-store.js +87 -0
- package/dist/claude/to-acp.js +19 -2
- package/dist/index.d.ts +4 -2
- package/dist/index.js +2 -1
- package/dist/meta.d.ts +19 -0
- package/dist/meta.js +19 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -109,6 +109,74 @@ implementations of one protocol disagree without anyone noticing. So:
|
|
|
109
109
|
line here can carry a prompt, a file or a credential, and a framing error is
|
|
110
110
|
not a reason to copy it into a log.
|
|
111
111
|
|
|
112
|
+
## Resuming a session: use the CLI's id, not ACP's
|
|
113
|
+
|
|
114
|
+
`session/load` works, and `initialize` reports `loadSession: true`. The trap is
|
|
115
|
+
which id to pass.
|
|
116
|
+
|
|
117
|
+
**ACP's `sessionId` is not resumable.** `session/new` returns an id this server
|
|
118
|
+
minted; the CLI has its own session id, a UUID, and `claude --resume` accepts
|
|
119
|
+
only that one (or a session title). The two are deliberately separate, and the
|
|
120
|
+
CLI's is published on the **first** `session/update` of every session:
|
|
121
|
+
|
|
122
|
+
```ts
|
|
123
|
+
import { META_CLI_SESSION_ID } from '@particle-academy/prism-acp';
|
|
124
|
+
|
|
125
|
+
// on the first session/update of a session
|
|
126
|
+
const cliSessionId = update._meta?.[META_CLI_SESSION_ID];
|
|
127
|
+
// store this against your own record -- it is what survives a restart
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
Then after a crash, a kill, or a restart:
|
|
131
|
+
|
|
132
|
+
```
|
|
133
|
+
session/load { sessionId: <the cli session id>, cwd: <absolute> }
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
Pass the ACP id instead and you get an error naming the key above, rather than a
|
|
137
|
+
success followed by a dead first prompt. That refusal exists because the CLI's
|
|
138
|
+
own complaint arrives one turn too late: it does error on an id it cannot
|
|
139
|
+
resume -- verified against claude 2.1.292 for both a non-UUID and a well-formed
|
|
140
|
+
UUID that does not exist, and it never silently starts a fresh conversation --
|
|
141
|
+
but by then `session/load` has already returned `{}` and you believe you have a
|
|
142
|
+
resumed session.
|
|
143
|
+
|
|
144
|
+
A session whose agent has **exited** is loadable; one whose agent is **still
|
|
145
|
+
running** is refused, by either id. No history is replayed on load, because the
|
|
146
|
+
CLI replays none -- `session/load` returning `{}` with no `session/update`
|
|
147
|
+
notifications is the honest report of that, not an omission.
|
|
148
|
+
|
|
149
|
+
### Refusing an unknown id at load, not a turn later
|
|
150
|
+
|
|
151
|
+
Pass `probeSession` and an id naming **no** conversation is refused by
|
|
152
|
+
`session/load` itself, instead of starting an agent that fails on its first
|
|
153
|
+
prompt:
|
|
154
|
+
|
|
155
|
+
```ts
|
|
156
|
+
import { probeSessionStore } from '@particle-academy/prism-acp';
|
|
157
|
+
|
|
158
|
+
const probeSession = (sessionId: string) => probeSessionStore(sessionId);
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
Pass that to `serve` beside `driverFactory` — see [Using it](#using-it).
|
|
162
|
+
|
|
163
|
+
It costs a directory listing, not a turn. The obvious probe -- resuming with a
|
|
164
|
+
throwaway prompt -- refuses a bad id for free but **answers** a good one, so it
|
|
165
|
+
would spend a real turn on every successful load.
|
|
166
|
+
|
|
167
|
+
**It can decline to answer, and that matters more than the refusal.** The result
|
|
168
|
+
is `present`, `absent` or `indeterminate`, and only `absent` refuses. The store's
|
|
169
|
+
layout is undocumented, so a store the probe cannot read -- a relocated home, a
|
|
170
|
+
permissions problem, a future CLI version -- reports `indeterminate` and the load
|
|
171
|
+
proceeds exactly as it did before. Calling a real session absent would refuse a
|
|
172
|
+
resume that would have worked, which is worse than the late error this replaces;
|
|
173
|
+
that error is still there as the backstop.
|
|
174
|
+
|
|
175
|
+
`probeSession` is yours to supply because the answer belongs to the agent being
|
|
176
|
+
driven, not to ACP: `probeSessionStore` reads claude's session store, and a Codex
|
|
177
|
+
driver would resolve the same question through `thread/resume`. Omit it and
|
|
178
|
+
`session/load` behaves as it always did.
|
|
179
|
+
|
|
112
180
|
## Rate limits are a gauge, not just a breach event
|
|
113
181
|
|
|
114
182
|
ACP has no field for a rate limit, so the detail rides in `_meta` under
|
|
@@ -176,6 +244,7 @@ serve({
|
|
|
176
244
|
input: process.stdin,
|
|
177
245
|
output: process.stdout,
|
|
178
246
|
driverFactory: (options, events) => new ClaudeDriver(options, events),
|
|
247
|
+
probeSession,
|
|
179
248
|
});
|
|
180
249
|
```
|
|
181
250
|
|
package/dist/acp/agent.d.ts
CHANGED
|
@@ -54,8 +54,22 @@ export type DriverFactory = (options: {
|
|
|
54
54
|
readonly cwd: string;
|
|
55
55
|
readonly resumeSessionId?: string;
|
|
56
56
|
}, events: DriverEvents) => AgentDriver;
|
|
57
|
+
/**
|
|
58
|
+
* Whether a session id names a conversation that can be resumed.
|
|
59
|
+
*
|
|
60
|
+
* Injected, and for two reasons. Tests must not read the developer's real
|
|
61
|
+
* session store; and the answer is a property of the AGENT being driven, not of
|
|
62
|
+
* ACP -- a Codex driver resolves it through `thread/resume`, not through
|
|
63
|
+
* claude's `~/.claude/projects` layout. Omit it and `session/load` behaves as it
|
|
64
|
+
* did before: it accepts the id and the agent reports the problem later.
|
|
65
|
+
*/
|
|
66
|
+
export type SessionProbe = (sessionId: string) => {
|
|
67
|
+
readonly existence: 'present' | 'absent' | 'indeterminate';
|
|
68
|
+
readonly detail: string;
|
|
69
|
+
};
|
|
57
70
|
export interface AcpAgentOptions {
|
|
58
71
|
readonly driverFactory: DriverFactory;
|
|
72
|
+
readonly probeSession?: SessionProbe;
|
|
59
73
|
readonly agentInfo?: {
|
|
60
74
|
readonly name: string;
|
|
61
75
|
readonly title?: string;
|
package/dist/acp/agent.js
CHANGED
|
@@ -22,13 +22,25 @@
|
|
|
22
22
|
* reported absent rather than optimistically.
|
|
23
23
|
*/
|
|
24
24
|
import { RPC_INVALID_PARAMS, RPC_INTERNAL_ERROR, RpcError } from '../jsonrpc.js';
|
|
25
|
+
import { META_CLI_SESSION_ID } from '../meta.js';
|
|
25
26
|
/** The protocol version this agent speaks. */
|
|
26
27
|
export const PROTOCOL_VERSION = 1;
|
|
28
|
+
/**
|
|
29
|
+
* The shape `#sessionNew` mints: `sess_<counter>_<epoch ms>`.
|
|
30
|
+
*
|
|
31
|
+
* Deliberately narrow. A broader "does not look like a UUID" test would refuse
|
|
32
|
+
* a session TITLE, which the CLI accepts alongside a UUID -- so this refuses
|
|
33
|
+
* only the ids this server is known to have handed out and which provably
|
|
34
|
+
* cannot resume.
|
|
35
|
+
*/
|
|
36
|
+
const MINTED_SESSION_ID = /^sess_\d+_\d+$/;
|
|
27
37
|
export class AcpAgent {
|
|
28
38
|
#peer;
|
|
29
39
|
#options;
|
|
30
40
|
#sessions = new Map();
|
|
31
41
|
#counter = 0;
|
|
42
|
+
/** Ids this process handed out via session/new. See the refusal in #sessionLoad. */
|
|
43
|
+
#minted = new Set();
|
|
32
44
|
constructor(peer, options) {
|
|
33
45
|
this.#peer = peer;
|
|
34
46
|
this.#options = options;
|
|
@@ -95,6 +107,7 @@ export class AcpAgent {
|
|
|
95
107
|
#sessionNew(params) {
|
|
96
108
|
const cwd = requireAbsoluteCwd(params);
|
|
97
109
|
const id = this.#options.newSessionId?.() ?? `sess_${++this.#counter}_${Date.now()}`;
|
|
110
|
+
this.#minted.add(id);
|
|
98
111
|
this.#open(id, cwd, undefined);
|
|
99
112
|
return { sessionId: id };
|
|
100
113
|
}
|
|
@@ -108,8 +121,77 @@ export class AcpAgent {
|
|
|
108
121
|
// Resuming an id this server already has open would leave two processes
|
|
109
122
|
// writing updates for one session, and the second would look like the
|
|
110
123
|
// first stuttering.
|
|
111
|
-
|
|
112
|
-
|
|
124
|
+
// Refused only while the session is genuinely LIVE. A session whose agent
|
|
125
|
+
// process has exited is the main thing anyone resumes -- a crashed or
|
|
126
|
+
// killed agent, or one lost to a restart -- and refusing that as "already
|
|
127
|
+
// open" described the map rather than reality: an exited session is never
|
|
128
|
+
// removed from it, only flagged. The old check made the one case resume
|
|
129
|
+
// exists for the one case it rejected.
|
|
130
|
+
const existing = this.#sessions.get(sessionId);
|
|
131
|
+
if (existing !== undefined && !existing.exited) {
|
|
132
|
+
throw new RpcError(RPC_INVALID_PARAMS, `session ${sessionId} is already open and its agent is still running`);
|
|
133
|
+
}
|
|
134
|
+
// The same refusal, by the CLI's id rather than ACP's.
|
|
135
|
+
//
|
|
136
|
+
// The sessions map is keyed by the ACP session id, so a lookup for a CLI id
|
|
137
|
+
// never matched a session opened by `session/new` -- meaning a client
|
|
138
|
+
// resuming a conversation whose agent was STILL RUNNING got a second agent
|
|
139
|
+
// on the same conversation, with no collision reported. That was
|
|
140
|
+
// unreachable while nothing could obtain a CLI id to resume with, and
|
|
141
|
+
// publishing the id is exactly what makes it reachable. Fixing the one
|
|
142
|
+
// without the other would have traded a dead end for two processes writing
|
|
143
|
+
// updates for one conversation.
|
|
144
|
+
for (const session of this.#sessions.values()) {
|
|
145
|
+
if (!session.exited && session.driver?.cliSessionId === sessionId) {
|
|
146
|
+
throw new RpcError(RPC_INVALID_PARAMS, `session ${sessionId} is already open as ${session.id} and its agent is still running`);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
// REFUSE an id this server minted, because `--resume` provably cannot take
|
|
150
|
+
// it: `session/new` returns an id of OUR making, the CLI has its own UUID,
|
|
151
|
+
// and only the CLI's works. A client that stored the id it was handed and
|
|
152
|
+
// passed it back here was the obvious thing to do and could never have
|
|
153
|
+
// worked.
|
|
154
|
+
//
|
|
155
|
+
// Refused HERE rather than left to the CLI, even though the CLI does error
|
|
156
|
+
// on it (verified: "is not a UUID and does not match any session title",
|
|
157
|
+
// and "No conversation found with session ID" for a well-formed one that
|
|
158
|
+
// does not exist -- it never silently starts a fresh conversation). That
|
|
159
|
+
// error arrives when the FIRST PROMPT runs, by which point `session/load`
|
|
160
|
+
// has already returned success and the client believes it has a resumed
|
|
161
|
+
// session. Moving the refusal to the load makes the failure land where the
|
|
162
|
+
// mistake was made, and the message can say what to pass instead -- which
|
|
163
|
+
// the CLI's cannot, because the CLI has never heard of ACP.
|
|
164
|
+
// Two tests, because neither alone is enough. The PATTERN catches the
|
|
165
|
+
// default mint and survives a restart, which is the case that matters most
|
|
166
|
+
// -- but an embedder supplying its own `newSessionId` is not covered by it.
|
|
167
|
+
// The SET catches any id this process actually handed out, whatever its
|
|
168
|
+
// shape, and does not survive a restart.
|
|
169
|
+
//
|
|
170
|
+
// Nothing covers "an id minted by a previous process using an injected
|
|
171
|
+
// generator", and nothing can: this server cannot tell such a string from a
|
|
172
|
+
// CLI session title by inspection. That residue is exactly why the CLI's
|
|
173
|
+
// own id is published in `_meta` rather than left to be guessed at.
|
|
174
|
+
if (MINTED_SESSION_ID.test(sessionId) || this.#minted.has(sessionId)) {
|
|
175
|
+
throw new RpcError(RPC_INVALID_PARAMS, `${sessionId} is an ACP session id minted by this server, which 'claude --resume' cannot accept. ` +
|
|
176
|
+
`Resume with the CLI's own session id, sent as '${META_CLI_SESSION_ID}' in the _meta of the first ` +
|
|
177
|
+
`session/update of the original session.`);
|
|
178
|
+
}
|
|
179
|
+
// REFUSE an id that names no conversation, here rather than a turn later.
|
|
180
|
+
//
|
|
181
|
+
// Without this the agent starts, `session/load` returns success, and the
|
|
182
|
+
// CLI's "No conversation found with session ID" arrives when the first
|
|
183
|
+
// prompt runs -- a real error, but one a client cannot tell from any other
|
|
184
|
+
// late failure, and one that lands after it has been told it holds a
|
|
185
|
+
// resumed session. A consumer reported this as the only thing standing
|
|
186
|
+
// between a bad id and a lost conversation.
|
|
187
|
+
//
|
|
188
|
+
// `indeterminate` deliberately PROCEEDS. The probe reads a store whose
|
|
189
|
+
// layout is undocumented, so a store it cannot read must not be allowed to
|
|
190
|
+
// refuse a resume that would have worked; the late error is still there as
|
|
191
|
+
// the backstop it always was. Only a positive `absent` refuses.
|
|
192
|
+
const probe = this.#options.probeSession?.(sessionId);
|
|
193
|
+
if (probe?.existence === 'absent') {
|
|
194
|
+
throw new RpcError(RPC_INVALID_PARAMS, `cannot resume ${sessionId}: ${probe.detail}`);
|
|
113
195
|
}
|
|
114
196
|
this.#open(sessionId, cwd, sessionId);
|
|
115
197
|
// The spec's result is an empty object; history arrives as session/update
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Three states, and the third is the point.
|
|
3
|
+
*
|
|
4
|
+
* `indeterminate` means the store could not be read -- a different CLI version,
|
|
5
|
+
* a relocated home, a permissions problem, a layout change. It must never be
|
|
6
|
+
* treated as `absent`: this check exists to turn a late failure into an early
|
|
7
|
+
* one, and it is not worth refusing a resume that would have worked.
|
|
8
|
+
*/
|
|
9
|
+
export type SessionExistence = 'present' | 'absent' | 'indeterminate';
|
|
10
|
+
export interface SessionProbe {
|
|
11
|
+
readonly existence: SessionExistence;
|
|
12
|
+
/** Why, in terms a client can act on. Empty for `present`. */
|
|
13
|
+
readonly detail: string;
|
|
14
|
+
}
|
|
15
|
+
export interface SessionStoreOptions {
|
|
16
|
+
/** Overridable so tests never read the developer's real sessions. */
|
|
17
|
+
readonly home?: string;
|
|
18
|
+
}
|
|
19
|
+
export declare function probeSessionStore(sessionId: string, options?: SessionStoreOptions): SessionProbe;
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
// Does a CLI session id name a conversation that exists?
|
|
2
|
+
//
|
|
3
|
+
// `session/load` used to answer this by trying. It spawned the agent with
|
|
4
|
+
// `--resume <id>` and the CLI's refusal -- `No conversation found with session
|
|
5
|
+
// ID: <id>` -- arrived when the FIRST PROMPT ran, by which point `session/load`
|
|
6
|
+
// had already returned success and the client believed it held a resumed
|
|
7
|
+
// session. The failure was real but indistinguishable from any other late
|
|
8
|
+
// error, and a consumer reported it as the only thing standing between a bad id
|
|
9
|
+
// and a lost conversation.
|
|
10
|
+
//
|
|
11
|
+
// WHY NOT PROBE WITH THE CLI. The obvious probe is
|
|
12
|
+
// `claude --resume <id> -p x`, which does refuse a bad id for free. On a GOOD
|
|
13
|
+
// id it resumes the conversation and answers "x" -- a real turn, real tokens,
|
|
14
|
+
// on every successful load. A probe that costs a turn on the happy path is
|
|
15
|
+
// worse than the problem it solves.
|
|
16
|
+
//
|
|
17
|
+
// So this reads the session store instead, which costs a directory listing.
|
|
18
|
+
//
|
|
19
|
+
// WHY SCAN RATHER THAN DERIVE THE PATH. Sessions live at
|
|
20
|
+
// `~/.claude/projects/<slug>/<uuid>.jsonl`, where `<slug>` is the project's cwd
|
|
21
|
+
// with its separators and punctuation replaced by `-`. Deriving that slug means
|
|
22
|
+
// reimplementing an undocumented rule, and getting it wrong would mean looking
|
|
23
|
+
// in a directory that does not exist and calling a VALID session absent --
|
|
24
|
+
// refusing a resume that would have worked, which is a worse failure than the
|
|
25
|
+
// late error this replaces. A session id is a UUID and therefore unique across
|
|
26
|
+
// every project, so looking for the FILE is enough and needs no slug at all.
|
|
27
|
+
import { readdirSync, statSync } from 'node:fs';
|
|
28
|
+
import { homedir } from 'node:os';
|
|
29
|
+
import { join } from 'node:path';
|
|
30
|
+
/** The CLI accepts a UUID, or a session TITLE, and nothing else. */
|
|
31
|
+
const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
32
|
+
export function probeSessionStore(sessionId, options = {}) {
|
|
33
|
+
// A non-UUID is refused by the CLI outright -- verified against claude
|
|
34
|
+
// 2.1.292: "Provided value ... is not a UUID and does not match any session
|
|
35
|
+
// title." Reported as absent without touching the disk, because the store
|
|
36
|
+
// cannot contain it under any layout.
|
|
37
|
+
//
|
|
38
|
+
// This is narrower than it looks: the CLI also accepts a session TITLE, and a
|
|
39
|
+
// title is not a UUID. It is reported absent anyway, because this server
|
|
40
|
+
// publishes the CLI's UUID as the thing to resume with and a client passing a
|
|
41
|
+
// human-chosen title is working from something else.
|
|
42
|
+
if (!UUID.test(sessionId)) {
|
|
43
|
+
return {
|
|
44
|
+
existence: 'absent',
|
|
45
|
+
detail: `${sessionId} is not a session id the CLI can resume: it is not a UUID.`,
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
const root = join(options.home ?? homedir(), '.claude', 'projects');
|
|
49
|
+
let projects;
|
|
50
|
+
try {
|
|
51
|
+
if (!statSync(root).isDirectory()) {
|
|
52
|
+
return { existence: 'indeterminate', detail: `${root} is not a directory` };
|
|
53
|
+
}
|
|
54
|
+
projects = readdirSync(root);
|
|
55
|
+
}
|
|
56
|
+
catch (error) {
|
|
57
|
+
return { existence: 'indeterminate', detail: `${root} could not be read: ${error.message}` };
|
|
58
|
+
}
|
|
59
|
+
// An EMPTY store is indeterminate rather than absent. A store with no
|
|
60
|
+
// projects in it is far more likely to be the wrong store than a genuine
|
|
61
|
+
// record that this conversation never existed.
|
|
62
|
+
if (projects.length === 0) {
|
|
63
|
+
return { existence: 'indeterminate', detail: `${root} lists no projects` };
|
|
64
|
+
}
|
|
65
|
+
const wanted = `${sessionId}.jsonl`;
|
|
66
|
+
let readable = 0;
|
|
67
|
+
for (const project of projects) {
|
|
68
|
+
let entries;
|
|
69
|
+
try {
|
|
70
|
+
entries = readdirSync(join(root, project));
|
|
71
|
+
}
|
|
72
|
+
catch {
|
|
73
|
+
continue; // One unreadable project does not settle the question.
|
|
74
|
+
}
|
|
75
|
+
readable++;
|
|
76
|
+
if (entries.includes(wanted))
|
|
77
|
+
return { existence: 'present', detail: '' };
|
|
78
|
+
}
|
|
79
|
+
// Every project directory failed to open. Absence has not been established.
|
|
80
|
+
if (readable === 0) {
|
|
81
|
+
return { existence: 'indeterminate', detail: `no project directory under ${root} could be read` };
|
|
82
|
+
}
|
|
83
|
+
return {
|
|
84
|
+
existence: 'absent',
|
|
85
|
+
detail: `no conversation with session id ${sessionId} exists in this installation's session store.`,
|
|
86
|
+
};
|
|
87
|
+
}
|
package/dist/claude/to-acp.js
CHANGED
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
* `{"index": 0, "delta": {...}}` and have no idea whether that is prose,
|
|
22
22
|
* reasoning, or the arguments to a command about to run.
|
|
23
23
|
*/
|
|
24
|
-
import { META_RATE_LIMIT, META_THINKING_SIGNATURE, META_THINKING_TOKENS_ESTIMATE, META_UNMAPPED_FRAME, withMeta, } from '../meta.js';
|
|
24
|
+
import { META_CLI_SESSION_ID, META_RATE_LIMIT, META_THINKING_SIGNATURE, META_THINKING_TOKENS_ESTIMATE, META_UNMAPPED_FRAME, withMeta, } from '../meta.js';
|
|
25
25
|
import { rateLimitNotice, readRateLimit } from './rate-limit.js';
|
|
26
26
|
/**
|
|
27
27
|
* ACP's ten tool kinds.
|
|
@@ -237,7 +237,24 @@ export class ClaudeToAcp {
|
|
|
237
237
|
return [
|
|
238
238
|
withMeta({ sessionUpdate: 'notice', notice: { level: 'debug', message: 'thinking' } }, { [META_THINKING_TOKENS_ESTIMATE]: input.estimated_tokens ?? null }),
|
|
239
239
|
];
|
|
240
|
-
case 'init':
|
|
240
|
+
case 'init': {
|
|
241
|
+
// The init frame carries the CLI's OWN session id, and that is the only
|
|
242
|
+
// string `--resume` accepts -- so it is the only string `session/load`
|
|
243
|
+
// can work with. It used to be recorded as unmapped, which meant the
|
|
244
|
+
// driver captured it, kept it correctly separate from ACP's sessionId,
|
|
245
|
+
// and then nothing handed it to the client. Resume was a method the
|
|
246
|
+
// client could not supply an argument for.
|
|
247
|
+
//
|
|
248
|
+
// Emitted on the FIRST frame of a session rather than at the end of a
|
|
249
|
+
// turn: a session that fails early still needs to be resumable, and a
|
|
250
|
+
// client cannot store what it was never sent.
|
|
251
|
+
const cliSessionId = asString(input.session_id);
|
|
252
|
+
if (cliSessionId === undefined)
|
|
253
|
+
return this.#unknown(input);
|
|
254
|
+
return [
|
|
255
|
+
withMeta({ sessionUpdate: 'notice', notice: { level: 'debug', message: 'session started' } }, { [META_CLI_SESSION_ID]: cliSessionId }),
|
|
256
|
+
];
|
|
257
|
+
}
|
|
241
258
|
case 'status':
|
|
242
259
|
case 'api_retry':
|
|
243
260
|
// Not mapped, but RECORDED. api_retry especially: it is how an
|
package/dist/index.d.ts
CHANGED
|
@@ -4,7 +4,7 @@ export { BASE_ALLOW, OUTRANKING_CREDENTIALS, childEnv } from './env.js';
|
|
|
4
4
|
export type { ChildEnvOptions, ChildEnvResult } from './env.js';
|
|
5
5
|
export { JsonRpcPeer, RPC_INTERNAL_ERROR, RPC_INVALID_PARAMS, RPC_INVALID_REQUEST, RPC_METHOD_NOT_FOUND, RPC_PARSE_ERROR, RpcError, } from './jsonrpc.js';
|
|
6
6
|
export type { JsonRpcPeerOptions, NotificationHandler, RequestHandler, RpcId, } from './jsonrpc.js';
|
|
7
|
-
export { META_NS, META_RATE_LIMIT, META_THINKING_SIGNATURE, META_THINKING_TOKENS_ESTIMATE, META_UNMAPPED_FRAME, RESERVED_META_KEYS, metaKey, withMeta, } from './meta.js';
|
|
7
|
+
export { META_CLI_SESSION_ID, META_NS, META_RATE_LIMIT, META_THINKING_SIGNATURE, META_THINKING_TOKENS_ESTIMATE, META_UNMAPPED_FRAME, RESERVED_META_KEYS, metaKey, withMeta, } from './meta.js';
|
|
8
8
|
export { ClaudeToAcp } from './claude/to-acp.js';
|
|
9
9
|
export type { AcpUpdate, ToolStatus } from './claude/to-acp.js';
|
|
10
10
|
export { parseRateLimit, rateLimitNotice, readRateLimit } from './claude/rate-limit.js';
|
|
@@ -12,7 +12,9 @@ export type { ClaudeRateLimit, ClaudeRateLimitWindow, RateLimitRead } from './cl
|
|
|
12
12
|
export { ClaudeDriver, claudeArgs, promptLine, updatesFromFrames } from './claude/driver.js';
|
|
13
13
|
export type { ClaudeDriverEvents, ClaudeDriverOptions, ClaudePermissionMode, } from './claude/driver.js';
|
|
14
14
|
export { AcpAgent, PROTOCOL_VERSION } from './acp/agent.js';
|
|
15
|
-
export type { AcpAgentOptions, AgentDriver, DriverEvents, DriverFactory, } from './acp/agent.js';
|
|
15
|
+
export type { AcpAgentOptions, AgentDriver, DriverEvents, DriverFactory, SessionProbe as AcpSessionProbe, } from './acp/agent.js';
|
|
16
|
+
export { probeSessionStore } from './claude/session-store.js';
|
|
17
|
+
export type { SessionExistence, SessionProbe, SessionStoreOptions } from './claude/session-store.js';
|
|
16
18
|
export { serve } from './acp/stdio.js';
|
|
17
19
|
export type { Served, ServeOptions } from './acp/stdio.js';
|
|
18
20
|
export { cliSessionIdOf, turnOutcomeOf } from './claude/driver.js';
|
package/dist/index.js
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
export { MAX_LINE_BYTES, NdjsonFramer, encodeLine, parseLine } from './ndjson.js';
|
|
2
2
|
export { BASE_ALLOW, OUTRANKING_CREDENTIALS, childEnv } from './env.js';
|
|
3
3
|
export { JsonRpcPeer, RPC_INTERNAL_ERROR, RPC_INVALID_PARAMS, RPC_INVALID_REQUEST, RPC_METHOD_NOT_FOUND, RPC_PARSE_ERROR, RpcError, } from './jsonrpc.js';
|
|
4
|
-
export { META_NS, META_RATE_LIMIT, META_THINKING_SIGNATURE, META_THINKING_TOKENS_ESTIMATE, META_UNMAPPED_FRAME, RESERVED_META_KEYS, metaKey, withMeta, } from './meta.js';
|
|
4
|
+
export { META_CLI_SESSION_ID, META_NS, META_RATE_LIMIT, META_THINKING_SIGNATURE, META_THINKING_TOKENS_ESTIMATE, META_UNMAPPED_FRAME, RESERVED_META_KEYS, metaKey, withMeta, } from './meta.js';
|
|
5
5
|
export { ClaudeToAcp } from './claude/to-acp.js';
|
|
6
6
|
export { parseRateLimit, rateLimitNotice, readRateLimit } from './claude/rate-limit.js';
|
|
7
7
|
export { ClaudeDriver, claudeArgs, promptLine, updatesFromFrames } from './claude/driver.js';
|
|
8
8
|
export { AcpAgent, PROTOCOL_VERSION } from './acp/agent.js';
|
|
9
|
+
export { probeSessionStore } from './claude/session-store.js';
|
|
9
10
|
export { serve } from './acp/stdio.js';
|
|
10
11
|
export { cliSessionIdOf, turnOutcomeOf } from './claude/driver.js';
|
package/dist/meta.d.ts
CHANGED
|
@@ -71,6 +71,25 @@ export declare const META_THINKING_TOKENS_ESTIMATE: string;
|
|
|
71
71
|
export declare const META_RATE_LIMIT: string;
|
|
72
72
|
/** The CLI frame a mapping could not place, kept so nothing is lost unseen. */
|
|
73
73
|
export declare const META_UNMAPPED_FRAME: string;
|
|
74
|
+
/**
|
|
75
|
+
* The CLI's OWN session id -- the only string `session/load` can resume with.
|
|
76
|
+
*
|
|
77
|
+
* This exists because its absence was a hole in the middle of resume. ACP's
|
|
78
|
+
* `sessionId` is minted by this server; the CLI has a different id of its own,
|
|
79
|
+
* a UUID, and `--resume` wants that one. The two were kept correctly separate
|
|
80
|
+
* inside the driver and then **never handed to the client**, so a client that
|
|
81
|
+
* stored the id `session/new` returned and passed it back to `session/load`
|
|
82
|
+
* was passing a string the CLI cannot resume.
|
|
83
|
+
*
|
|
84
|
+
* ACP has no field for a provider's internal session id, which is precisely
|
|
85
|
+
* what `_meta` is for. It rides on the `init` frame's notice, so it is the
|
|
86
|
+
* first thing a client learns about a session -- a client that only reads it
|
|
87
|
+
* after the first turn would miss it on a session that fails early.
|
|
88
|
+
*
|
|
89
|
+
* Store it against your own record of the session. It is what you pass to
|
|
90
|
+
* `session/load` after a restart, and ACP's own `sessionId` is not.
|
|
91
|
+
*/
|
|
92
|
+
export declare const META_CLI_SESSION_ID: string;
|
|
74
93
|
/**
|
|
75
94
|
* Attach `_meta` entries to an ACP object without disturbing its own fields.
|
|
76
95
|
*
|
package/dist/meta.js
CHANGED
|
@@ -83,6 +83,25 @@ export const META_THINKING_TOKENS_ESTIMATE = metaKey('thinking_tokens_estimate')
|
|
|
83
83
|
export const META_RATE_LIMIT = metaKey('rate_limit');
|
|
84
84
|
/** The CLI frame a mapping could not place, kept so nothing is lost unseen. */
|
|
85
85
|
export const META_UNMAPPED_FRAME = metaKey('unmapped_frame');
|
|
86
|
+
/**
|
|
87
|
+
* The CLI's OWN session id -- the only string `session/load` can resume with.
|
|
88
|
+
*
|
|
89
|
+
* This exists because its absence was a hole in the middle of resume. ACP's
|
|
90
|
+
* `sessionId` is minted by this server; the CLI has a different id of its own,
|
|
91
|
+
* a UUID, and `--resume` wants that one. The two were kept correctly separate
|
|
92
|
+
* inside the driver and then **never handed to the client**, so a client that
|
|
93
|
+
* stored the id `session/new` returned and passed it back to `session/load`
|
|
94
|
+
* was passing a string the CLI cannot resume.
|
|
95
|
+
*
|
|
96
|
+
* ACP has no field for a provider's internal session id, which is precisely
|
|
97
|
+
* what `_meta` is for. It rides on the `init` frame's notice, so it is the
|
|
98
|
+
* first thing a client learns about a session -- a client that only reads it
|
|
99
|
+
* after the first turn would miss it on a session that fails early.
|
|
100
|
+
*
|
|
101
|
+
* Store it against your own record of the session. It is what you pass to
|
|
102
|
+
* `session/load` after a restart, and ACP's own `sessionId` is not.
|
|
103
|
+
*/
|
|
104
|
+
export const META_CLI_SESSION_ID = metaKey('cli_session_id');
|
|
86
105
|
/**
|
|
87
106
|
* Attach `_meta` entries to an ACP object without disturbing its own fields.
|
|
88
107
|
*
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@particle-academy/prism-acp",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "Speak the Agent Client Protocol to a coding-agent CLI the user has already authenticated. No API key, no third-party adapter.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|