@anhlp/alp-runtime 0.0.0-beta.2 → 0.0.1-beta.3
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/dist/claude-transport.d.ts +3 -0
- package/dist/claude-transport.js +59 -8
- package/dist/contracts.d.ts +2 -1
- package/dist/runtime-context.d.ts +1 -1
- package/dist/runtime-context.js +1 -1
- package/dist/runtime.js +244 -122
- package/dist/session-config.d.ts +24 -0
- package/dist/session-config.js +3 -3
- package/dist/session-state.d.ts +8 -1
- package/dist/tool-detail.js +4 -1
- package/dist/tools.d.ts +8 -0
- package/dist/tools.js +7 -5
- package/package.json +3 -3
- package/src/claude-transport.ts +64 -8
- package/src/contracts.ts +2 -2
- package/src/runtime-context.ts +2 -2
- package/src/runtime.ts +261 -115
- package/src/session-config.ts +5 -2
- package/src/session-state.ts +8 -1
- package/src/tool-detail.ts +4 -1
- package/src/tools.ts +6 -4
package/dist/session-config.d.ts
CHANGED
|
@@ -392,6 +392,10 @@ export declare function nativeSessionConfig(runtimeKind: RuntimeKind, mapping: R
|
|
|
392
392
|
type: string;
|
|
393
393
|
description: string;
|
|
394
394
|
};
|
|
395
|
+
requestId: {
|
|
396
|
+
type: string;
|
|
397
|
+
description: string;
|
|
398
|
+
};
|
|
395
399
|
};
|
|
396
400
|
required: string[];
|
|
397
401
|
additionalProperties: boolean;
|
|
@@ -427,6 +431,10 @@ export declare function nativeSessionConfig(runtimeKind: RuntimeKind, mapping: R
|
|
|
427
431
|
type: string;
|
|
428
432
|
description: string;
|
|
429
433
|
};
|
|
434
|
+
requestId: {
|
|
435
|
+
type: string;
|
|
436
|
+
description: string;
|
|
437
|
+
};
|
|
430
438
|
labels: {
|
|
431
439
|
type: string;
|
|
432
440
|
items: {
|
|
@@ -1034,6 +1042,10 @@ export declare function nativeSessionConfig(runtimeKind: RuntimeKind, mapping: R
|
|
|
1034
1042
|
type: string;
|
|
1035
1043
|
description: string;
|
|
1036
1044
|
};
|
|
1045
|
+
requestId: {
|
|
1046
|
+
type: string;
|
|
1047
|
+
description: string;
|
|
1048
|
+
};
|
|
1037
1049
|
};
|
|
1038
1050
|
required: string[];
|
|
1039
1051
|
additionalProperties: boolean;
|
|
@@ -1069,6 +1081,10 @@ export declare function nativeSessionConfig(runtimeKind: RuntimeKind, mapping: R
|
|
|
1069
1081
|
type: string;
|
|
1070
1082
|
description: string;
|
|
1071
1083
|
};
|
|
1084
|
+
requestId: {
|
|
1085
|
+
type: string;
|
|
1086
|
+
description: string;
|
|
1087
|
+
};
|
|
1072
1088
|
labels: {
|
|
1073
1089
|
type: string;
|
|
1074
1090
|
items: {
|
|
@@ -1678,6 +1694,10 @@ export declare function nativeSessionConfig(runtimeKind: RuntimeKind, mapping: R
|
|
|
1678
1694
|
type: string;
|
|
1679
1695
|
description: string;
|
|
1680
1696
|
};
|
|
1697
|
+
requestId: {
|
|
1698
|
+
type: string;
|
|
1699
|
+
description: string;
|
|
1700
|
+
};
|
|
1681
1701
|
};
|
|
1682
1702
|
required: string[];
|
|
1683
1703
|
additionalProperties: boolean;
|
|
@@ -1713,6 +1733,10 @@ export declare function nativeSessionConfig(runtimeKind: RuntimeKind, mapping: R
|
|
|
1713
1733
|
type: string;
|
|
1714
1734
|
description: string;
|
|
1715
1735
|
};
|
|
1736
|
+
requestId: {
|
|
1737
|
+
type: string;
|
|
1738
|
+
description: string;
|
|
1739
|
+
};
|
|
1716
1740
|
labels: {
|
|
1717
1741
|
type: string;
|
|
1718
1742
|
items: {
|
package/dist/session-config.js
CHANGED
|
@@ -97,7 +97,7 @@ function nativeSessionConfig(runtimeKind, mapping, targets, parentAgent, role, s
|
|
|
97
97
|
dynamicTools: [SEND_TOOL, BOARD_TOOL, taskTool(taskActions(mapping, parentAgent, role))]
|
|
98
98
|
};
|
|
99
99
|
}
|
|
100
|
-
const delegationInstruction = targets.length ? `Use alp_delegate to assign bounded work to: ${targets.join(", ")}. Work runs in the background: by default alp_delegate starts the assignment and returns its assignmentId at once, and its result, questions and stall reports come to you as mail. Pass wait: true only when your very next step cannot go on without the result; it then returns the result, or early with the child's first question. A result carries the child's structured handoff (null if it filed none) and output, its final message. While assignments run, go on with work that does not depend on them; when only their results are left, alp_wait for them (mail from your requester or the user ends the wait early), or end your turn and ALP wakes you with their results and questions. Answer questions with alp_send kind answer and replyTo; use kind steer to change an instruction, note for information. To stop an assignment, for example one parked on a usage limit whose work should go on now, use alp_cancel and delegate the rest, on another runtime (pass a model of claude: or codex:); its changes stay in the checkout or its worktree. At most ${mapping.workflow.maxPeers} peers may run concurrently. Concurrent peers must be read-only or isolated: pass isolation "worktree" to give a writing peer its own git worktree. A worktree result lists its branch and changed files; apply it with alp_merge (uncommitted, conflicts left as markers) or drop it with alp_discard, then verify. Writers in this shared checkout run one at a time. To learn why a finished assignment did something, ask it with alp_recall rather than guessing from its handoff. When the project configures verify commands, ALP runs them before alp_merge applies a change, and after a writer in your checkout finishes; a failed check applies nothing. To fix a failed worktree change, delegate again with continueFrom set to its assignmentId. Do not run shell/file mutations in parallel with delegation. Include scope, constraints, verification, and required handoff in task. Child inherits your mode unless you request read-only. Review its returned evidence before answering. Do not use native spawn tools or shell-launched agents to bypass this route.` : "No delegation targets are authorized. Do not spawn agents or use shell-launched agents. Complete your assigned scope and return evidence.";
|
|
100
|
+
const delegationInstruction = targets.length ? `Use alp_delegate to assign bounded work to: ${targets.join(", ")}. Delegation has two finite budgets shared by the entire root session tree: 16 implementation starts (lead/peer) and separately 16 read-only advisor starts (reviewer/oracle). Only a new accepted user prompt in root, including steering, resets both; child turns, mail wakes and approval answers do not. Started assignments count even when they fail. Work runs in the background: by default alp_delegate starts the assignment and returns its assignmentId at once, and its result, questions and stall reports come to you as mail. Pass wait: true only when your very next step cannot go on without the result; it then returns the result, or early with the child's first question. A result carries the child's structured handoff (null if it filed none) and output, its final message. While assignments run, go on with work that does not depend on them; when only their results are left, alp_wait for them (mail from your requester or the user ends the wait early), or end your turn and ALP wakes you with their results and questions. Answer questions with alp_send kind answer and replyTo; use kind steer to change an instruction, note for information. To stop an assignment, for example one parked on a usage limit whose work should go on now, use alp_cancel and delegate the rest, on another runtime (pass a model of claude: or codex:); its changes stay in the checkout or its worktree. At most ${mapping.workflow.maxPeers} peers may run concurrently. Concurrent peers must be read-only or isolated: pass isolation "worktree" to give a writing peer its own git worktree. A worktree result lists its branch and changed files; apply it with alp_merge (uncommitted, conflicts left as markers) or drop it with alp_discard, then verify. Writers in this shared checkout run one at a time. To learn why a finished assignment did something, ask it with alp_recall rather than guessing from its handoff. When the project configures verify commands, ALP runs them before alp_merge applies a change, and after a writer in your checkout finishes; a failed check applies nothing. To fix a failed worktree change, delegate again with continueFrom set to its assignmentId. Do not run shell/file mutations in parallel with delegation. Include scope, constraints, verification, and required handoff in task. Child inherits your mode unless you request read-only. Review its returned evidence before answering. Do not use native spawn tools or shell-launched agents to bypass this route.` : "No delegation targets are authorized. Do not spawn agents or use shell-launched agents. Complete your assigned scope and return evidence.";
|
|
101
101
|
return {
|
|
102
102
|
runtime: runtimeKind,
|
|
103
103
|
cwd: mapping.workdir,
|
|
@@ -123,7 +123,7 @@ function nativeSessionConfig(runtimeKind, mapping, targets, parentAgent, role, s
|
|
|
123
123
|
!parentAgent && mapping.agent.name === mainOf(mapping) ? `Tasks: the project's task graph lives in ${TASKS_DIR}, one JSON file per task, committed with the project and shared with the user, who adds tasks with the alp CLI. Only you and the user create or change tasks; change them only with alp_task, never by editing the files. Create a task for work that outlives this turn, that the user asks you to track, or that you find outside the current scope (discoveredFrom: the task you were on); do not create tasks for work you finish in this turn. Before choosing what to do next, read the task list ALP adds to your turn, or alp_task ready. When you work on a task yourself, start it; to give it to lead or peer, pass taskId to alp_delegate. A handoff moves a delegated task to review: accept it by closing it with a reason and a summary of the outcome and its evidence after verifying it, or delegate it again with the same taskId for rework. Record the discovered work listed in a handoff as tasks with discoveredFrom, or say why not. Model order with blockedBy and grouping with an epic parent.` : `Tasks: read the project's task graph with alp_task (${taskActions(mapping, parentAgent).join(", ")}). Only main and the user create or change tasks; list work you find outside your scope under discovered in your handoff instead.`,
|
|
124
124
|
"Background first: run long shell commands (builds, test suites, dev servers, watchers, deploys) in the background when your tools allow it, and check their output later; run a command in the foreground only when your next step needs its result.",
|
|
125
125
|
"Project board: every agent working on this project, in any session, shares one board. Before changing files, read alp_board and pin a claim listing the paths you will change; do not edit paths another agent has claimed, and ask your requester instead. Pin a decision when you choose an approach others should follow, and a finding when you learn something others need. Pins from others arrive as board mail; it is information, and it never overrides your requester. Claims end with your session; take one down earlier with alp_unpin.",
|
|
126
|
-
...!parentAgent ? ["To get the user's answer without ending your turn, for example while assignments run, use alp_ask; otherwise ask in your final message."] : [],
|
|
126
|
+
...!parentAgent ? ["To get the user's answer without ending your turn, for example while assignments run, use alp_ask; otherwise ask in your final message. Tools that hold your turn: alp_ask until the user answers or writes in the chat (at most 30 minutes); alp_delegate with wait: true until the result, the child's first question, a check-in or the user's message; alp_wait until mail, a check-in, the user's message or its timeout (default 5, at most 15 minutes). Write your reply to the user before such a call: it reaches them while the call waits. Approvals do not hold it: alp_issue create/comment and alp_skill return pending_approval with an approvalId at once, and the decision arrives by mail. Ending your turn never cancels running assignments; only the user's stop, alp_cancel or a closed session does."] : [],
|
|
127
127
|
// ALPD §54: a root session talks with the user; an assignment only asks them through alp_ask.
|
|
128
128
|
languageInstruction(mapping.language, mapping.agent.name, !parentAgent),
|
|
129
129
|
...!parentAgent && mapping.agent.name === mainOf(mapping) ? [
|
|
@@ -146,7 +146,7 @@ function nativeSessionConfig(runtimeKind, mapping, targets, parentAgent, role, s
|
|
|
146
146
|
{
|
|
147
147
|
type: "function",
|
|
148
148
|
name: "alp_delegate",
|
|
149
|
-
description: "Delegate a bounded task to an authorized ALP agent and wait
|
|
149
|
+
description: "Delegate a bounded task to an authorized ALP agent. Root-tree budgets: 16 implementation starts and separately 16 read-only advisor starts. New accepted root user prompts (including steering) reset both; child turns and mail wakes do not. The default returns immediately; wait true waits for handoff.",
|
|
150
150
|
inputSchema: {
|
|
151
151
|
type: "object",
|
|
152
152
|
properties: {
|
package/dist/session-state.d.ts
CHANGED
|
@@ -11,6 +11,8 @@ export type Session = {
|
|
|
11
11
|
mapping: ResolvedSession;
|
|
12
12
|
threadId: string;
|
|
13
13
|
active?: string;
|
|
14
|
+
/** A continued turn accepts a simultaneous user auto prompt as steering. */
|
|
15
|
+
resumedTurn?: string;
|
|
14
16
|
pending: boolean;
|
|
15
17
|
buffered: Array<[string, any]>;
|
|
16
18
|
closed: boolean;
|
|
@@ -26,12 +28,17 @@ export type Session = {
|
|
|
26
28
|
peerCount: number;
|
|
27
29
|
/** Live assignments this session requested, keyed by child session id. */
|
|
28
30
|
assignments: Map<string, Assignment>;
|
|
31
|
+
/** Root-tree implementation starts since the last new root user turn. */
|
|
29
32
|
calls: number;
|
|
33
|
+
/** Separate finite budget for mandatory read-only reviewer/oracle starts. */
|
|
34
|
+
advisorCalls: number;
|
|
30
35
|
mail: MailEvent[];
|
|
31
36
|
waiters: Waiter[];
|
|
32
37
|
lastActivity: number;
|
|
33
|
-
/** Auto-wakes since the last user prompt
|
|
38
|
+
/** Auto-wakes since the last user prompt or new result/question; check-ins do not count. */
|
|
34
39
|
wakes: number;
|
|
40
|
+
/** Failed wake starts since fresh result/question/check-in mail or a user prompt. */
|
|
41
|
+
wakeFailures: number;
|
|
35
42
|
/** When it last got a check-in, or when its assignments started running. */
|
|
36
43
|
checkInAt?: number;
|
|
37
44
|
/** Set by interrupt: pending mail waits for the next user prompt. */
|
package/dist/tool-detail.js
CHANGED
|
@@ -257,7 +257,10 @@ function antigravityToolDetail(info) {
|
|
|
257
257
|
return { type: "unknown", input: tool?.parameters ?? null, output: tool?.error?.message ?? output ?? null };
|
|
258
258
|
}
|
|
259
259
|
function withHeaders(path, diff) {
|
|
260
|
-
|
|
260
|
+
if (/^(--- |diff --git )/.test(diff)) return diff;
|
|
261
|
+
if (diff.startsWith("+++ ")) return `--- a/${path}
|
|
262
|
+
${diff}`;
|
|
263
|
+
return `--- a/${path}
|
|
261
264
|
+++ b/${path}
|
|
262
265
|
${diff}`;
|
|
263
266
|
}
|
package/dist/tools.d.ts
CHANGED
|
@@ -418,6 +418,10 @@ export declare const SKILL_TOOL: {
|
|
|
418
418
|
type: string;
|
|
419
419
|
description: string;
|
|
420
420
|
};
|
|
421
|
+
requestId: {
|
|
422
|
+
type: string;
|
|
423
|
+
description: string;
|
|
424
|
+
};
|
|
421
425
|
};
|
|
422
426
|
required: string[];
|
|
423
427
|
additionalProperties: boolean;
|
|
@@ -454,6 +458,10 @@ export declare const ISSUE_TOOL: {
|
|
|
454
458
|
type: string;
|
|
455
459
|
description: string;
|
|
456
460
|
};
|
|
461
|
+
requestId: {
|
|
462
|
+
type: string;
|
|
463
|
+
description: string;
|
|
464
|
+
};
|
|
457
465
|
labels: {
|
|
458
466
|
type: string;
|
|
459
467
|
items: {
|
package/dist/tools.js
CHANGED
|
@@ -64,7 +64,7 @@ var HANDOFF_TOOL = {
|
|
|
64
64
|
var WAIT_TOOL = {
|
|
65
65
|
type: "function",
|
|
66
66
|
name: "alp_wait",
|
|
67
|
-
description: "Wait for mail from your assignments: results, questions, notes, and stall reports. Returns as soon as any arrives, or with an empty list and a status snapshot
|
|
67
|
+
description: "Wait for mail from your assignments: results, questions, notes, and stall reports. Returns as soon as any arrives, a check-in comes or the user writes to you; otherwise with an empty list and a status snapshot after timeoutMs (default 5 minutes, at most 15). Ending your turn instead never cancels your assignments.",
|
|
68
68
|
inputSchema: {
|
|
69
69
|
type: "object",
|
|
70
70
|
properties: {
|
|
@@ -196,7 +196,7 @@ var UNPIN_TOOL = {
|
|
|
196
196
|
var ASK_TOOL = {
|
|
197
197
|
type: "function",
|
|
198
198
|
name: "alp_ask",
|
|
199
|
-
description: "Ask a question and wait for the answer: your requester, or the user. Only main talks to the user, unless the user has written to you.
|
|
199
|
+
description: "Ask a question and wait for the answer: your requester, or the user. Only main talks to the user, unless the user has written to you. Holds your turn until answered; returns unanswered after the ask timeout (15 minutes for a requester, 30 for the user), and user_wrote when the user writes in the chat instead of answering.",
|
|
200
200
|
inputSchema: {
|
|
201
201
|
type: "object",
|
|
202
202
|
properties: {
|
|
@@ -225,7 +225,7 @@ var LESSON_TOOL = {
|
|
|
225
225
|
var SKILL_TOOL = {
|
|
226
226
|
type: "function",
|
|
227
227
|
name: "alp_skill",
|
|
228
|
-
description: "Propose a skill distilled from your lessons. ALP shows the whole skill to the user, and saves it to the user's skill library and gives it to the roles only when the user approves;
|
|
228
|
+
description: "Propose a skill distilled from your lessons. ALP shows the whole skill to the user, and saves it to the user's skill library and gives it to the roles only when the user approves. Returns pending_approval with approvalId immediately; the decision (saved, or not saved with the user's feedback) arrives once by mail, even after your turn ends. Continue other work. A retry of the same draft returns its existing pending/result in this session; use a fresh requestId only to propose an identical draft again.",
|
|
229
229
|
inputSchema: {
|
|
230
230
|
type: "object",
|
|
231
231
|
properties: {
|
|
@@ -234,7 +234,8 @@ var SKILL_TOOL = {
|
|
|
234
234
|
body: { type: "string", description: "The SKILL.md body in Markdown, without frontmatter: when to use it, the method as steps, and the checks." },
|
|
235
235
|
roles: { type: "array", items: { type: "string" }, description: "The roles whose work the skill guides, and only those: any of main (yourself), lead, peer, oracle, reviewer, supervisor, or a custom agent of this project." },
|
|
236
236
|
lessons: { type: "array", items: { type: "string" }, description: "The exact text of the lessons this skill replaces; they leave the lessons files once it is saved." },
|
|
237
|
-
replace: { type: "boolean", description: "Propose a new version of an existing skill of this name." }
|
|
237
|
+
replace: { type: "boolean", description: "Propose a new version of an existing skill of this name." },
|
|
238
|
+
requestId: { type: "string", description: "Optional session-scoped idempotency key (1-100 characters). Reuse with the same draft on retries; a fresh key explicitly proposes a new operation." }
|
|
238
239
|
},
|
|
239
240
|
required: ["name", "description", "body", "roles"],
|
|
240
241
|
additionalProperties: false
|
|
@@ -243,7 +244,7 @@ var SKILL_TOOL = {
|
|
|
243
244
|
var ISSUE_TOOL = {
|
|
244
245
|
type: "function",
|
|
245
246
|
name: "alp_issue",
|
|
246
|
-
description: "GitHub issues of this project (target project, from its origin remote) or of ALP itself (target alp). search needs no approval. create and comment show the whole draft to the user and post it only when the user approves;
|
|
247
|
+
description: "GitHub issues of this project (target project, from its origin remote) or of ALP itself (target alp). search needs no approval. create and comment show the whole draft to the user and post it only when the user approves; create/comment return pending_approval with approvalId immediately, then send one decision/posting result by mail, even after your turn ends. Continue other work. Draft retries return the existing pending/result in this session. Use a fresh requestId only to intentionally propose an identical draft again; reuse one requestId with the exact draft to replay safely.",
|
|
247
248
|
inputSchema: {
|
|
248
249
|
type: "object",
|
|
249
250
|
properties: {
|
|
@@ -253,6 +254,7 @@ var ISSUE_TOOL = {
|
|
|
253
254
|
title: { type: "string", description: "For create." },
|
|
254
255
|
body: { type: "string", description: "For create and comment: Markdown. Facts, reproduction and evidence; no secrets." },
|
|
255
256
|
issue: { type: "integer", description: "For comment: the issue number." },
|
|
257
|
+
requestId: { type: "string", description: "Optional session-scoped idempotency key (1-100 characters). Reuse with the same draft on retries; a fresh key explicitly proposes a new operation." },
|
|
256
258
|
labels: { type: "array", items: { type: "string" }, description: "For create: existing labels of the repository." }
|
|
257
259
|
},
|
|
258
260
|
required: ["action", "target"],
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@anhlp/alp-runtime",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.1-beta.3",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "ALP runtime module",
|
|
6
6
|
"files": [
|
|
@@ -222,8 +222,8 @@
|
|
|
222
222
|
"node": ">=22.12"
|
|
223
223
|
},
|
|
224
224
|
"dependencies": {
|
|
225
|
-
"@anhlp/alp-core": "0.0.
|
|
226
|
-
"@anhlp/alp-protocol": "0.0.
|
|
225
|
+
"@anhlp/alp-core": "0.0.1-beta.3",
|
|
226
|
+
"@anhlp/alp-protocol": "0.0.1-beta.3",
|
|
227
227
|
"@anthropic-ai/claude-agent-sdk": "^0.3.292",
|
|
228
228
|
"@modelcontextprotocol/sdk": "^1.32.1",
|
|
229
229
|
"@agentclientprotocol/sdk": "1.7.0",
|
package/src/claude-transport.ts
CHANGED
|
@@ -68,11 +68,13 @@ export const toolShapes: Record<string, Record<string, z.ZodType>> = {
|
|
|
68
68
|
roles: z.array(z.string().min(1)).min(1).max(10),
|
|
69
69
|
lessons: z.array(z.string().min(1).max(600)).max(50).optional(),
|
|
70
70
|
replace: z.boolean().optional(),
|
|
71
|
+
requestId: z.string().min(1).max(100).optional(),
|
|
71
72
|
},
|
|
72
73
|
alp_issue: {
|
|
73
74
|
action: z.enum(['search', 'create', 'comment']),
|
|
74
75
|
target: z.enum(['project', 'alp']),
|
|
75
76
|
query: z.string().min(1).max(200).optional(),
|
|
77
|
+
requestId: z.string().min(1).max(100).optional(),
|
|
76
78
|
title: z.string().min(1).max(200).optional(),
|
|
77
79
|
body: z.string().min(1).max(20_000).optional(),
|
|
78
80
|
issue: z.number().int().positive().optional(),
|
|
@@ -344,7 +346,8 @@ export class ClaudeTransport {
|
|
|
344
346
|
private threadId = '';
|
|
345
347
|
private activeTurn?: string;
|
|
346
348
|
private closed = false;
|
|
347
|
-
private assistantText = new Map<string, string>();
|
|
349
|
+
private assistantText = new Map<string, { blocks: Map<number, { text: string; final: boolean }>; seen: Set<string> }>();
|
|
350
|
+
private streamingMessage?: string;
|
|
348
351
|
private toolCalls = new Map<string, { name: string; command: string; input: unknown }>();
|
|
349
352
|
/** The last rate limit Claude reported, and whether the running turn hit one. */
|
|
350
353
|
private rateLimit?: { status: string; resetsAt?: number; overageStatus?: string; rateLimitType?: string; utilization?: number };
|
|
@@ -433,6 +436,7 @@ export class ClaudeTransport {
|
|
|
433
436
|
}
|
|
434
437
|
|
|
435
438
|
if (method === 'thread/start' || method === 'thread/resume' || method === 'thread/fork') {
|
|
439
|
+
this.clearAssistantText();
|
|
436
440
|
this.config = params as NativeConfig;
|
|
437
441
|
this.threadId =
|
|
438
442
|
method === 'thread/resume' ? params.threadId : randomUUID();
|
|
@@ -451,7 +455,10 @@ export class ClaudeTransport {
|
|
|
451
455
|
const turnId = method === 'turn/steer'
|
|
452
456
|
? params.expectedTurnId
|
|
453
457
|
: randomUUID();
|
|
454
|
-
if (method === 'turn/start')
|
|
458
|
+
if (method === 'turn/start') {
|
|
459
|
+
this.clearAssistantText();
|
|
460
|
+
this.activeTurn = turnId;
|
|
461
|
+
}
|
|
455
462
|
this.input.push({
|
|
456
463
|
type: 'user',
|
|
457
464
|
message: {
|
|
@@ -466,6 +473,7 @@ export class ClaudeTransport {
|
|
|
466
473
|
}
|
|
467
474
|
|
|
468
475
|
if (method === 'turn/interrupt') {
|
|
476
|
+
this.streamingMessage = undefined;
|
|
469
477
|
await this.query?.interrupt();
|
|
470
478
|
return {};
|
|
471
479
|
}
|
|
@@ -574,7 +582,7 @@ export class ClaudeTransport {
|
|
|
574
582
|
...(plugins.length ? { plugins } : {}),
|
|
575
583
|
persistSession: !config.ephemeral,
|
|
576
584
|
promptSuggestions: false,
|
|
577
|
-
includePartialMessages:
|
|
585
|
+
includePartialMessages: true,
|
|
578
586
|
...(from ? { resume: from.resume, ...(from.fork ? { forkSession: true, sessionId: this.threadId } : {}) } : { sessionId: this.threadId }),
|
|
579
587
|
// Explicit native path avoids SDK optional binaries inside Electron app.asar.
|
|
580
588
|
pathToClaudeCodeExecutable: this.command,
|
|
@@ -620,6 +628,7 @@ export class ClaudeTransport {
|
|
|
620
628
|
}
|
|
621
629
|
|
|
622
630
|
private handle(message: SDKMessage) {
|
|
631
|
+
if (this.closed) return;
|
|
623
632
|
if (message.type === 'rate_limit_event') {
|
|
624
633
|
this.rateLimit = message.rate_limit_info;
|
|
625
634
|
this.emit('account/rateLimits/updated', { claude: message.rate_limit_info });
|
|
@@ -627,10 +636,36 @@ export class ClaudeTransport {
|
|
|
627
636
|
}
|
|
628
637
|
// Claude starts a turn by itself when a background task it ran ends (ALPD §56). Adopt it, so
|
|
629
638
|
// ALP's tools work in it and its end is reported like any other turn's.
|
|
630
|
-
|
|
639
|
+
const streamStart = message.type === 'stream_event' && message.event?.type === 'message_start';
|
|
640
|
+
if ((message.type === 'assistant' || streamStart) && !message.parent_tool_use_id && !this.activeTurn) {
|
|
631
641
|
this.activeTurn = randomUUID();
|
|
632
642
|
this.emit('turn/started', { threadId: this.threadId, turn: { id: this.activeTurn } });
|
|
633
643
|
}
|
|
644
|
+
if (message.type === 'stream_event') {
|
|
645
|
+
if (message.parent_tool_use_id) return;
|
|
646
|
+
const event = message.event;
|
|
647
|
+
if (event.type === 'message_start') {
|
|
648
|
+
this.streamingMessage = event.message.id;
|
|
649
|
+
} else if (event.type === 'message_stop') {
|
|
650
|
+
this.streamingMessage = undefined;
|
|
651
|
+
} else if (this.streamingMessage) {
|
|
652
|
+
const id = this.streamingMessage;
|
|
653
|
+
const state = this.assistantState(id);
|
|
654
|
+
let delta: string | undefined;
|
|
655
|
+
if (event.type === 'content_block_start' && event.content_block.type === 'text') {
|
|
656
|
+
state.blocks.set(event.index, { text: event.content_block.text, final: false });
|
|
657
|
+
delta = event.content_block.text;
|
|
658
|
+
} else if (event.type === 'content_block_delta' && event.delta.type === 'text_delta') {
|
|
659
|
+
const block = state.blocks.get(event.index);
|
|
660
|
+
if (block && !block.final) {
|
|
661
|
+
block.text += event.delta.text;
|
|
662
|
+
delta = event.delta.text;
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
if (delta) this.emit('item/agentMessage/delta', { threadId: this.threadId, turnId: this.activeTurn, itemId: id, delta });
|
|
666
|
+
}
|
|
667
|
+
return;
|
|
668
|
+
}
|
|
634
669
|
if (message.type === 'assistant' && message.error === 'rate_limit') this.limitedTurn = true;
|
|
635
670
|
// A compaction, reported as Codex reports one: a contextCompaction item from start to end (ALPD §57).
|
|
636
671
|
if (message.type === 'system' && (message.subtype === 'status' || message.subtype === 'compact_boundary')) {
|
|
@@ -671,14 +706,21 @@ export class ClaudeTransport {
|
|
|
671
706
|
if (windows.length && !this.compactAt) this.contextWindow = Math.max(...windows);
|
|
672
707
|
}
|
|
673
708
|
if (message.type === 'assistant' && !message.parent_tool_use_id) {
|
|
709
|
+
const id = message.message.id;
|
|
710
|
+
const state = this.assistantState(id);
|
|
711
|
+
if (message.uuid && state.seen.has(message.uuid)) return;
|
|
712
|
+
if (message.uuid) state.seen.add(message.uuid);
|
|
674
713
|
for (const block of message.message.content as any[]) {
|
|
675
714
|
if (block.type === 'text') {
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
715
|
+
// SDK 0.3.292 delivers one authoritative assistant event per completed
|
|
716
|
+
// block, sharing message.id. Replace that block's draft, never append
|
|
717
|
+
// the final text to its streamed draft or replace earlier final blocks.
|
|
718
|
+
const pending = [...state.blocks.entries()].sort(([a], [b]) => a - b).find(([, value]) => !value.final);
|
|
719
|
+
const index = pending?.[0] ?? Math.max(-1, ...state.blocks.keys()) + 1;
|
|
720
|
+
state.blocks.set(index, { text: block.text, final: true });
|
|
679
721
|
this.emit('item/completed', {
|
|
680
722
|
threadId: this.threadId,
|
|
681
|
-
item: { type: 'agentMessage', id, text },
|
|
723
|
+
item: { type: 'agentMessage', id, text: [...state.blocks.entries()].sort(([a], [b]) => a - b).map(([, block]) => block.text).join('') },
|
|
682
724
|
});
|
|
683
725
|
} else if (block.type === 'tool_use') {
|
|
684
726
|
const command = block.name === 'Bash'
|
|
@@ -737,6 +779,7 @@ export class ClaudeTransport {
|
|
|
737
779
|
return;
|
|
738
780
|
}
|
|
739
781
|
|
|
782
|
+
if (message.type === 'result') this.clearAssistantText();
|
|
740
783
|
if (message.type === 'result' && this.activeTurn) {
|
|
741
784
|
const turnId = this.activeTurn;
|
|
742
785
|
this.activeTurn = undefined;
|
|
@@ -764,8 +807,20 @@ export class ClaudeTransport {
|
|
|
764
807
|
for (const listener of this.listeners) listener(method, params);
|
|
765
808
|
}
|
|
766
809
|
|
|
810
|
+
private assistantState(id: string) {
|
|
811
|
+
let state = this.assistantText.get(id);
|
|
812
|
+
if (!state) this.assistantText.set(id, state = { blocks: new Map(), seen: new Set() });
|
|
813
|
+
return state;
|
|
814
|
+
}
|
|
815
|
+
|
|
816
|
+
private clearAssistantText() {
|
|
817
|
+
this.streamingMessage = undefined;
|
|
818
|
+
this.assistantText.clear();
|
|
819
|
+
}
|
|
820
|
+
|
|
767
821
|
private fail(value: unknown) {
|
|
768
822
|
if (this.closed) return;
|
|
823
|
+
this.clearAssistantText();
|
|
769
824
|
const error = value instanceof Error ? value : new Error(String(value));
|
|
770
825
|
for (const listener of this.failures) listener(error);
|
|
771
826
|
}
|
|
@@ -773,6 +828,7 @@ export class ClaudeTransport {
|
|
|
773
828
|
async close() {
|
|
774
829
|
if (this.closed) return;
|
|
775
830
|
this.closed = true;
|
|
831
|
+
this.clearAssistantText();
|
|
776
832
|
this.input.close();
|
|
777
833
|
this.query?.close();
|
|
778
834
|
await this.pump?.catch(() => {});
|
package/src/contracts.ts
CHANGED
|
@@ -147,8 +147,8 @@ export type AlpRuntime = {
|
|
|
147
147
|
recall(target: { assignmentId?: string; taskId?: string; projectRoot?: string }, question: string): Promise<RecallAnswer>;
|
|
148
148
|
/** Assignments an earlier alpd left running, which recover() can continue once their root is open again. */
|
|
149
149
|
recoverable(): LiveEntry[];
|
|
150
|
-
/** Continues
|
|
151
|
-
recover(rootId: string, options?: { continueRoot?: boolean }): Promise<RecoveryOutcome[]>;
|
|
150
|
+
/** Continues work in a reopened tree. continueRoot resumes its running turn; parkedRoot restores its held continuation intent. */
|
|
151
|
+
recover(rootId: string, options?: { continueRoot?: boolean; parkedRoot?: string }): Promise<RecoveryOutcome[]>;
|
|
152
152
|
/** Gives up the assignments of a tree whose root cannot be opened again: their tasks go back to open. */
|
|
153
153
|
abandon(rootId: string, reason: string): Promise<RecoveryOutcome[]>;
|
|
154
154
|
/** Closes everything; assignments still running stay in the live file, so the next alpd continues them. */
|
package/src/runtime-context.ts
CHANGED
|
@@ -35,13 +35,13 @@ export function claudeUsage(result: any) {
|
|
|
35
35
|
|
|
36
36
|
/**
|
|
37
37
|
* Whether a runtime's usage report (codexUsage or claudeUsage) lets it work again (ALPD §58):
|
|
38
|
-
* true when every window has room, false
|
|
38
|
+
* true when every window has room, false on explicit provider denial, a used-up window or spending cap, and
|
|
39
39
|
* undefined when the report says nothing either way.
|
|
40
40
|
*/
|
|
41
41
|
export function usageAllows(usage: any): boolean | undefined {
|
|
42
42
|
if (!usage?.available) return undefined;
|
|
43
43
|
const limits: any[] = Array.isArray(usage.limits) ? usage.limits : [];
|
|
44
|
-
if (limits.some(limit => limit?.spendControlReached === true)) return false;
|
|
44
|
+
if (usage.ordinaryUsageAllowed === false || limits.some(limit => limit?.spendControlReached === true)) return false;
|
|
45
45
|
const windows: any[] = limits.length ? limits.flatMap(limit => limit?.windows ?? []) : usage.windows ?? [];
|
|
46
46
|
const known = windows.filter(window => typeof window?.usedPercent === 'number');
|
|
47
47
|
if (!known.length) return undefined;
|