@anhlp/alp-runtime 0.0.0-beta.1 → 0.0.0-beta.2
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/acp-transport.d.ts +16 -0
- package/dist/acp-transport.js +40 -7
- package/dist/antigravity-transport.d.ts +2 -0
- package/dist/antigravity-transport.js +12 -2
- package/dist/claude-transport.d.ts +14 -0
- package/dist/claude-transport.js +21 -9
- package/dist/contracts.d.ts +8 -2
- package/dist/contracts.js +2 -2
- package/dist/index.d.ts +2 -0
- package/dist/index.js +14 -0
- package/dist/resolve.js +3 -3
- package/dist/runtime.js +54 -24
- package/dist/session-state.d.ts +3 -0
- package/dist/tool-detail.d.ts +81 -0
- package/dist/tool-detail.js +329 -0
- package/dist/usage.d.ts +10 -0
- package/dist/usage.js +26 -1
- package/package.json +3 -3
- package/src/acp-transport.ts +44 -9
- package/src/antigravity-transport.ts +9 -0
- package/src/claude-transport.ts +24 -8
- package/src/contracts.ts +8 -2
- package/src/index.ts +2 -0
- package/src/resolve.ts +3 -3
- package/src/runtime.ts +65 -25
- package/src/session-state.ts +3 -0
- package/src/tool-detail.ts +394 -0
- package/src/usage.ts +37 -0
|
@@ -7,6 +7,7 @@ import os from 'node:os';
|
|
|
7
7
|
import path from 'node:path';
|
|
8
8
|
import { createInterface, type Interface } from 'node:readline';
|
|
9
9
|
import { openMcpTools } from './mcp-tools.js';
|
|
10
|
+
import { antigravityToolDetail } from './tool-detail.js';
|
|
10
11
|
|
|
11
12
|
/**
|
|
12
13
|
* Antigravity provider definition for ALP.
|
|
@@ -72,6 +73,8 @@ export function encodePrompt(text: string): string {
|
|
|
72
73
|
* Emits line-delimited JSON events and parses stdout line by line.
|
|
73
74
|
*/
|
|
74
75
|
export class AntigravityTransport {
|
|
76
|
+
/** Antigravity's prompt is one line of text: no images. */
|
|
77
|
+
takesImages() { return false; }
|
|
75
78
|
private child?: ChildProcessWithoutNullStreams;
|
|
76
79
|
private lines?: Interface;
|
|
77
80
|
private exit: Promise<void> = Promise.resolve();
|
|
@@ -383,6 +386,9 @@ export class AntigravityTransport {
|
|
|
383
386
|
const toolName = step.tool_name || 'tool';
|
|
384
387
|
const callId = `agy-tool-${this.activeTurn}-${step.step_index ?? ++this.messages}`;
|
|
385
388
|
|
|
389
|
+
// The command stays the tool's name, as the journal reads it; the typed detail rides along.
|
|
390
|
+
const detail = antigravityToolDetail(step.tool_info ?? { name: toolName });
|
|
391
|
+
const typed = { tool: typeof step.tool_info?.name === 'string' && step.tool_info.name ? step.tool_info.name : toolName, detail };
|
|
386
392
|
if (step.state === 'ACTIVE') {
|
|
387
393
|
this.emit('item/started', {
|
|
388
394
|
threadId: this.conversationId,
|
|
@@ -393,6 +399,7 @@ export class AntigravityTransport {
|
|
|
393
399
|
cwd: this.cwd,
|
|
394
400
|
status: 'inProgress',
|
|
395
401
|
aggregatedOutput: '',
|
|
402
|
+
...typed,
|
|
396
403
|
},
|
|
397
404
|
});
|
|
398
405
|
} else if (step.state === 'DONE' || step.state === 'ERROR') {
|
|
@@ -409,6 +416,8 @@ export class AntigravityTransport {
|
|
|
409
416
|
status: step.state === 'DONE' ? 'completed' : 'failed',
|
|
410
417
|
exitCode: step.state === 'DONE' ? 0 : 1,
|
|
411
418
|
aggregatedOutput: output,
|
|
419
|
+
...typed,
|
|
420
|
+
...(step.state === 'ERROR' ? { error: step.tool_info?.error?.message } : {}),
|
|
412
421
|
},
|
|
413
422
|
});
|
|
414
423
|
}
|
package/src/claude-transport.ts
CHANGED
|
@@ -6,6 +6,7 @@ import { optionalRead, claudeUsage } from './runtime-context.js';
|
|
|
6
6
|
import { randomUUID } from 'node:crypto';
|
|
7
7
|
import { z } from 'zod';
|
|
8
8
|
import { PIN_KINDS } from './board.js';
|
|
9
|
+
import { claudeToolDetail, resultText } from './tool-detail.js';
|
|
9
10
|
import { CLOSE_REASONS, GATE_KINDS, TASK_STATUSES, TASK_TYPES } from '@anhlp/alp-core/tasks';
|
|
10
11
|
import { commandDecision } from '@anhlp/alp-core/permissions';
|
|
11
12
|
|
|
@@ -217,6 +218,13 @@ export function claudeSandboxAvailable() {
|
|
|
217
218
|
|
|
218
219
|
const WRITERS = ['Edit', 'Write', 'MultiEdit', 'NotebookEdit'];
|
|
219
220
|
/** What ALP asks the user for a permission Claude would otherwise not have. */
|
|
221
|
+
/** One turn-input part as Claude's: text, or a data-URL image as a base64 image block. */
|
|
222
|
+
export function claudeContent(part: any) {
|
|
223
|
+
const image = part?.type === 'image' && typeof part.url === 'string' ? /^data:(image\/(?:png|jpeg|gif|webp));base64,([A-Za-z0-9+/]+={0,2})$/.exec(part.url) : null;
|
|
224
|
+
if (part?.type === 'image' && !image) throw new Error('Claude takes images as inline base64 PNG, JPEG, GIF or WebP only');
|
|
225
|
+
return image ? { type: 'image', source: { type: 'base64', media_type: image[1], data: image[2] } } : { type: 'text', text: part.text };
|
|
226
|
+
}
|
|
227
|
+
|
|
220
228
|
export type PermissionRequest = { tool: string; input: Record<string, unknown>; reason: 'rule' | 'mode'; rule?: string };
|
|
221
229
|
export type PermissionAnswer = { allow: boolean; always?: boolean; message?: string };
|
|
222
230
|
|
|
@@ -337,7 +345,7 @@ export class ClaudeTransport {
|
|
|
337
345
|
private activeTurn?: string;
|
|
338
346
|
private closed = false;
|
|
339
347
|
private assistantText = new Map<string, string>();
|
|
340
|
-
private toolCalls = new Map<string, { name: string; command: string }>();
|
|
348
|
+
private toolCalls = new Map<string, { name: string; command: string; input: unknown }>();
|
|
341
349
|
/** The last rate limit Claude reported, and whether the running turn hit one. */
|
|
342
350
|
private rateLimit?: { status: string; resetsAt?: number; overageStatus?: string; rateLimitType?: string; utilization?: number };
|
|
343
351
|
private limitedTurn = false;
|
|
@@ -448,10 +456,7 @@ export class ClaudeTransport {
|
|
|
448
456
|
type: 'user',
|
|
449
457
|
message: {
|
|
450
458
|
role: 'user',
|
|
451
|
-
content: params.input.map(
|
|
452
|
-
type: 'text',
|
|
453
|
-
text: part.text,
|
|
454
|
-
})),
|
|
459
|
+
content: params.input.map(claudeContent),
|
|
455
460
|
},
|
|
456
461
|
parent_tool_use_id: null,
|
|
457
462
|
uuid: randomUUID(),
|
|
@@ -658,6 +663,9 @@ export class ClaudeTransport {
|
|
|
658
663
|
});
|
|
659
664
|
}
|
|
660
665
|
if (message.type === 'result') {
|
|
666
|
+
// The session's cost so far, as Claude counts it; a viewer shows it with the context meter.
|
|
667
|
+
const cost = (message as any).total_cost_usd;
|
|
668
|
+
if (typeof cost === 'number' && Number.isFinite(cost) && cost >= 0) this.emit('thread/tokenUsage/updated', { threadId: this.threadId, turnId: this.activeTurn, tokenUsage: { totalCostUsd: cost } });
|
|
661
669
|
const windows = Object.values((message as any).modelUsage ?? {}).map((entry: any) => entry?.contextWindow).filter((value): value is number => typeof value === 'number' && value > 0);
|
|
662
670
|
// Only when it was not measured: the measured window may be the setting's, smaller than the model's.
|
|
663
671
|
if (windows.length && !this.compactAt) this.contextWindow = Math.max(...windows);
|
|
@@ -676,7 +684,9 @@ export class ClaudeTransport {
|
|
|
676
684
|
const command = block.name === 'Bash'
|
|
677
685
|
? String(block.input?.command ?? '')
|
|
678
686
|
: `${block.name} ${JSON.stringify(block.input ?? {})}`;
|
|
679
|
-
this.toolCalls.set(block.id, { name: block.name, command });
|
|
687
|
+
this.toolCalls.set(block.id, { name: block.name, command, input: block.input });
|
|
688
|
+
// The command stays what skill use, copy escapes and the journal read; the typed detail rides along.
|
|
689
|
+
const detail = claudeToolDetail(block.name, block.input);
|
|
680
690
|
this.emit('item/started', {
|
|
681
691
|
threadId: this.threadId,
|
|
682
692
|
item: {
|
|
@@ -686,6 +696,7 @@ export class ClaudeTransport {
|
|
|
686
696
|
cwd: this.cwd,
|
|
687
697
|
status: 'inProgress',
|
|
688
698
|
aggregatedOutput: '',
|
|
699
|
+
...(detail ? { tool: block.name, detail } : {}),
|
|
689
700
|
},
|
|
690
701
|
});
|
|
691
702
|
}
|
|
@@ -697,13 +708,17 @@ export class ClaudeTransport {
|
|
|
697
708
|
const content = Array.isArray(message.message.content)
|
|
698
709
|
? message.message.content as any[]
|
|
699
710
|
: [];
|
|
700
|
-
|
|
701
|
-
|
|
711
|
+
// The SDK's structured output belongs to the message's one tool result.
|
|
712
|
+
const results = content.filter(block => block.type === 'tool_result');
|
|
713
|
+
const structured = results.length === 1 ? (message as any).tool_use_result : undefined;
|
|
714
|
+
for (const block of results) {
|
|
702
715
|
const call = this.toolCalls.get(block.tool_use_id);
|
|
703
716
|
if (!call) continue;
|
|
704
717
|
const output = typeof block.content === 'string'
|
|
705
718
|
? block.content
|
|
706
719
|
: JSON.stringify(block.content ?? '');
|
|
720
|
+
const text = resultText(block.content);
|
|
721
|
+
const detail = claudeToolDetail(call.name, call.input, { text, structured, error: Boolean(block.is_error) });
|
|
707
722
|
this.emit('item/completed', {
|
|
708
723
|
threadId: this.threadId,
|
|
709
724
|
item: {
|
|
@@ -714,6 +729,7 @@ export class ClaudeTransport {
|
|
|
714
729
|
status: block.is_error ? 'failed' : 'completed',
|
|
715
730
|
exitCode: block.is_error ? 1 : 0,
|
|
716
731
|
aggregatedOutput: output,
|
|
732
|
+
...(detail ? { tool: call.name, detail, ...(block.is_error ? { error: text } : {}) } : {}),
|
|
717
733
|
},
|
|
718
734
|
});
|
|
719
735
|
this.toolCalls.delete(block.tool_use_id);
|
package/src/contracts.ts
CHANGED
|
@@ -16,6 +16,8 @@ export type RuntimeTransport = {
|
|
|
16
16
|
onFailure(listener: (error: unknown) => void): void;
|
|
17
17
|
close(): Promise<void>;
|
|
18
18
|
onRequest?: (listener: (method: string, params: any) => Promise<unknown>) => void;
|
|
19
|
+
/** Whether turn input may hold images (`{ type: 'image', url: 'data:…' }`); omitted, Codex and Claude do. */
|
|
20
|
+
takesImages?: () => boolean;
|
|
19
21
|
};
|
|
20
22
|
|
|
21
23
|
export type RuntimeOptions = {
|
|
@@ -156,8 +158,12 @@ export type AlpRuntime = {
|
|
|
156
158
|
import type { SessionPreview } from '@anhlp/alp-protocol/session';
|
|
157
159
|
export type { SessionPreview } from '@anhlp/alp-protocol/session';
|
|
158
160
|
|
|
159
|
-
/**
|
|
160
|
-
|
|
161
|
+
/**
|
|
162
|
+
* The team `alp init` names in a new project's settings (core INIT_TEAM). Kept for compatibility;
|
|
163
|
+
* it is not the default of a session: preview and create both resolve that through core
|
|
164
|
+
* `defaultTeam` (settings without workflow.mode run `custom`).
|
|
165
|
+
*/
|
|
166
|
+
export { INIT_TEAM as DEFAULT_TEAM } from '@anhlp/alp-core/workflow';
|
|
161
167
|
|
|
162
168
|
import type { RecoveryOutcome, Pause, PauseState } from '@anhlp/alp-protocol/session';
|
|
163
169
|
export type { RecoveryOutcome, Pause, PauseState } from '@anhlp/alp-protocol/session';
|
package/src/index.ts
CHANGED
|
@@ -24,3 +24,5 @@ export type { Pin, PinKind } from './board.js';
|
|
|
24
24
|
|
|
25
25
|
export { ProviderRegistry, createProviderRegistry } from './providers.js';
|
|
26
26
|
export type { ProviderContext, ProviderFactory, ProviderFactories } from './providers.js';
|
|
27
|
+
export { acpToolDetail, antigravityToolDetail, boundDetail, claudeToolDetail, clipText, codexToolCall, stripReadGutter, toolError, DETAIL_FILE_PATHS, DETAIL_TEXT_CHARS, DETAIL_WEB_RESULTS, ERROR_CHARS, TOOL_FAILED } from './tool-detail.js';
|
|
28
|
+
export type { AcpCall, ClaudeResult, CodexToolCall, ToolCallDetail } from './tool-detail.js';
|
package/src/resolve.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import path from 'node:path';
|
|
2
2
|
import { access, readFile, stat } from 'node:fs/promises';
|
|
3
|
-
import { resolveWorkflow } from '@anhlp/alp-core/workflow';
|
|
3
|
+
import { defaultTeam, resolveWorkflow } from '@anhlp/alp-core/workflow';
|
|
4
4
|
import { initProject } from '@anhlp/alp-core/init';
|
|
5
5
|
import { profileModel, agentProfiles, selectedProfile } from '@anhlp/alp-core/profiles';
|
|
6
6
|
import { jsonObject, resolveAgent } from '@anhlp/alp-core/resolver';
|
|
@@ -95,13 +95,13 @@ export async function resolveSession(spec: SessionSpec, options: { templates?: R
|
|
|
95
95
|
if (scaffoldMissing) await initProject(spec.cwd, { ...starter, library: options.library });
|
|
96
96
|
else if (!spec.restore) {
|
|
97
97
|
const settings = await jsonObject(settingsFile, 'INVALID_SETTINGS');
|
|
98
|
-
await initProject(spec.cwd, { ...starter, library: options.library, onlyManagedProfiles: true, teamIds: [
|
|
98
|
+
await initProject(spec.cwd, { ...starter, library: options.library, onlyManagedProfiles: true, teamIds: [spec.workflow === undefined ? defaultTeam(settings) : teamOf(spec.workflow)] });
|
|
99
99
|
}
|
|
100
100
|
}
|
|
101
101
|
if (options.library) await ensureLibrary(options.library, options.templates ? { templates: options.templates } : {});
|
|
102
102
|
const restored = spec.restore;
|
|
103
103
|
if (restored && spec.agent !== undefined && spec.agent !== restored.agent) throw new Error('Cannot resume a thread as a different ALP agent');
|
|
104
|
-
const { team, ...workflow } = await resolveWorkflow(spec.cwd, spec.workflow, restored?.workflow, { library: options.library, templates: options.templates });
|
|
104
|
+
const { team, ...workflow } = await resolveWorkflow(spec.cwd, spec.workflow, restored?.workflow, { library: options.library, templates: options.templates, scaffold: options.initialize === false });
|
|
105
105
|
const agent = await resolveAgent(spec.cwd, { agent: spec.agent ?? restored?.agent, library: options.library, templates: options.templates });
|
|
106
106
|
const oracleModels = team?.oracleModels ?? ORACLE_MODELS;
|
|
107
107
|
if (agent.name === 'oracle' && !restored && !oracleModels.includes(spec.model ?? '')) throw new Error(`Oracle runs on ${oracleModels.join(' or ')}; choose one`);
|
package/src/runtime.ts
CHANGED
|
@@ -2,9 +2,10 @@ import { constants as fsConstants } from 'node:fs';
|
|
|
2
2
|
import type { Session, Assignment, Waiter } from './session-state.js';
|
|
3
3
|
import { errorData, plainObject, toolResult } from './tool-results.js';
|
|
4
4
|
import { createLogger } from '@anhlp/alp-core/log';
|
|
5
|
+
import { PROMPT_IMAGES, base64Bytes } from '@anhlp/alp-protocol';
|
|
5
6
|
import { createBoardDomain } from './board-domain.js';
|
|
6
7
|
import { createWorktreeDomain } from './worktree-domain.js';
|
|
7
|
-
import {
|
|
8
|
+
import { type RuntimeTransport, type RuntimeOptions, type OpenOptions, type PromptContent, type PromptInput, type AlpRuntime, type SessionPreview, type RecoveryOutcome, type Pause, type PauseState, type RecallAnswer } from './contracts.js';
|
|
8
9
|
export type { RuntimeTransport, RuntimeOptions, OpenOptions, PromptContent, PromptInput, AlpRuntime, SessionPreview, RecoveryOutcome, Pause, PauseState, RecallAnswer } from './contracts.js';
|
|
9
10
|
export { DEFAULT_TEAM } from './contracts.js';
|
|
10
11
|
import { verdictLine, parseHandoff, type Handoff } from './handoff.js';
|
|
@@ -40,9 +41,10 @@ import { OWN_START, sameProcessAlive } from './process-info.js';
|
|
|
40
41
|
import { createRecallBook, recallPrompt, RECALL_KEEP_MS, RECALL_QUESTION_CHARS, RECALL_TIMEOUT_MS, type RecallEntry } from './recall.js';
|
|
41
42
|
import { branchExists, checkoutFingerprint, checkoutKey, createCopy, createWorktree, reattachWorktree, removeCopy, removeWorktree } from './workspace.js';
|
|
42
43
|
import { claudeSandboxAvailable, SKILL_PLUGIN } from './claude-transport.js';
|
|
44
|
+
import { boundDetail, codexToolCall, toolError } from './tool-detail.js';
|
|
43
45
|
import { ALP_REPO, gh, projectRepo } from './github.js';
|
|
44
46
|
import { optionalRead, usageAllows, COMPACT_SHARE } from './runtime-context.js';
|
|
45
|
-
import { providerUsage, type ProviderUsage } from './usage.js';
|
|
47
|
+
import { providerUsage, sessionUsage, type ProviderUsage } from './usage.js';
|
|
46
48
|
import { parse as toml } from 'smol-toml';
|
|
47
49
|
import { findFormula, formulaDirs, listFormulas, pourFormula } from '@anhlp/alp-core/formulas';
|
|
48
50
|
import { addAllowRule, capMode, commandDecision, profileFor, unwrapShell } from '@anhlp/alp-core/permissions';
|
|
@@ -183,6 +185,11 @@ export function createAlpRuntime(options: RuntimeOptions = {}): AlpRuntime {
|
|
|
183
185
|
return run;
|
|
184
186
|
}
|
|
185
187
|
|
|
188
|
+
/** Whether the session's runtime takes images in a prompt: the transport says, else Codex and Claude do. */
|
|
189
|
+
function takesImages(session: Session) {
|
|
190
|
+
return session.runtime.takesImages ? session.runtime.takesImages() : session.runtimeKind === 'codex' || session.runtimeKind === 'claude';
|
|
191
|
+
}
|
|
192
|
+
|
|
186
193
|
function snapshot(sessionId: string, session: Session): SessionSnapshot {
|
|
187
194
|
return {
|
|
188
195
|
id: sessionId,
|
|
@@ -201,6 +208,8 @@ export function createAlpRuntime(options: RuntimeOptions = {}): AlpRuntime {
|
|
|
201
208
|
...(session.active ? { activeTurnId: session.active } : {}),
|
|
202
209
|
...(session.parked ? { parked: session.parked.reason } : {}),
|
|
203
210
|
...(session.instructionsSha ? { instructionsSha: session.instructionsSha } : {}),
|
|
211
|
+
...(takesImages(session) ? { promptImages: true } : {}),
|
|
212
|
+
...(session.usage ? { usage: session.usage } : {}),
|
|
204
213
|
busy: !!(session.active || session.pending || session.children.size || session.assignments.size || hasActiveMail(session) || reviewing(session)),
|
|
205
214
|
};
|
|
206
215
|
}
|
|
@@ -273,6 +282,7 @@ export function createAlpRuntime(options: RuntimeOptions = {}): AlpRuntime {
|
|
|
273
282
|
sessionId: string,
|
|
274
283
|
session: Session,
|
|
275
284
|
item: any,
|
|
285
|
+
done = false,
|
|
276
286
|
) {
|
|
277
287
|
if (item.type === 'agentMessage') {
|
|
278
288
|
// The first words main shows after the user wrote: how long the user waited.
|
|
@@ -290,7 +300,8 @@ export function createAlpRuntime(options: RuntimeOptions = {}): AlpRuntime {
|
|
|
290
300
|
.filter((c: any) => c.type === 'text')
|
|
291
301
|
.map((c: any) => c.text)
|
|
292
302
|
.join('\n');
|
|
293
|
-
|
|
303
|
+
const images = (item.content ?? []).filter((c: any) => c.type === 'image' || c.type === 'localImage').length;
|
|
304
|
+
emit(sessionId, { type: 'item', item: { kind: 'user_message', id: item.id, text, ...(images ? { images } : {}) } });
|
|
294
305
|
return;
|
|
295
306
|
}
|
|
296
307
|
|
|
@@ -335,24 +346,37 @@ export function createAlpRuntime(options: RuntimeOptions = {}): AlpRuntime {
|
|
|
335
346
|
session.journal.push(stamp(`${session.mapping.agent.name} shell ${status}${item.exitCode != null ? ` (exit ${item.exitCode})` : ''}: ${clip(String(item.command ?? ''))}`));
|
|
336
347
|
}
|
|
337
348
|
|
|
349
|
+
// A transport that knows the tool attaches its typed detail; the command stays as it was.
|
|
350
|
+
// The protocol's bounds hold here for every transport (boundDetail passes unknown as it is).
|
|
351
|
+
const typed = item.detail && typeof item.detail === 'object' && item.detail.type !== 'shell' ? boundDetail(item.detail) : undefined;
|
|
338
352
|
emit(sessionId, {
|
|
339
353
|
type: 'item',
|
|
340
354
|
item: {
|
|
341
355
|
kind: 'tool_call',
|
|
342
356
|
id: item.id,
|
|
343
357
|
callId: item.id,
|
|
344
|
-
name: 'shell',
|
|
358
|
+
name: typed ? String(item.tool || 'tool') : 'shell',
|
|
345
359
|
status,
|
|
346
|
-
...(status === 'failed' ? { error: 'Command failed' } : {}),
|
|
347
|
-
detail: {
|
|
360
|
+
...(status === 'failed' ? { error: typed ? toolError(item.error) : 'Command failed' } : {}),
|
|
361
|
+
detail: typed ?? (item.detail?.type === 'shell' ? item.detail : {
|
|
348
362
|
type: 'shell',
|
|
349
363
|
command: item.command,
|
|
350
364
|
cwd: item.cwd,
|
|
351
365
|
output: item.aggregatedOutput ?? '',
|
|
352
366
|
exitCode: item.exitCode,
|
|
353
|
-
},
|
|
367
|
+
}),
|
|
354
368
|
},
|
|
355
369
|
});
|
|
370
|
+
return;
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
// Codex's own file changes, web searches, MCP calls and sub-agents (ALPD §4.2).
|
|
374
|
+
const call = codexToolCall(item, done);
|
|
375
|
+
if (call) {
|
|
376
|
+
emit(sessionId, {
|
|
377
|
+
type: 'item',
|
|
378
|
+
item: { kind: 'tool_call', id: item.id, callId: item.id, name: call.name, status: call.status, ...(call.error ? { error: call.error } : {}), detail: call.detail },
|
|
379
|
+
});
|
|
356
380
|
}
|
|
357
381
|
}
|
|
358
382
|
|
|
@@ -390,19 +414,30 @@ export function createAlpRuntime(options: RuntimeOptions = {}): AlpRuntime {
|
|
|
390
414
|
if (method === 'item/completed' || method === 'item/started') {
|
|
391
415
|
if (method === 'item/completed') session.progressAt = Date.now();
|
|
392
416
|
if (params.item?.type !== 'userMessage') {
|
|
393
|
-
nativeItem(sessionId, session, params.item);
|
|
417
|
+
nativeItem(sessionId, session, params.item, method === 'item/completed');
|
|
394
418
|
}
|
|
395
419
|
if (method === 'item/started' && params.item?.type === 'commandExecution') watchCopy(sessionId, session, params.item);
|
|
396
420
|
if (method === 'item/started' && params.item) skillUse(sessionId, session, params.item);
|
|
397
421
|
return;
|
|
398
422
|
}
|
|
399
423
|
|
|
424
|
+
// A running tool call that learned more, such as an ACP call's path or diff.
|
|
425
|
+
if (method === 'item/updated') {
|
|
426
|
+
if (params.item?.type === 'commandExecution' && params.item.status === 'inProgress') nativeItem(sessionId, session, params.item);
|
|
427
|
+
return;
|
|
428
|
+
}
|
|
429
|
+
|
|
400
430
|
if (method === 'account/rateLimits/updated') {
|
|
401
431
|
usageReport(session, params);
|
|
402
432
|
return;
|
|
403
433
|
}
|
|
404
434
|
|
|
405
435
|
if (method === 'thread/tokenUsage/updated') {
|
|
436
|
+
const usage = sessionUsage(session.usage, params?.tokenUsage);
|
|
437
|
+
if (usage) {
|
|
438
|
+
session.usage = usage;
|
|
439
|
+
emit(sessionId, { type: 'usage', usage });
|
|
440
|
+
}
|
|
406
441
|
contextReport(sessionId, session, params?.tokenUsage);
|
|
407
442
|
return;
|
|
408
443
|
}
|
|
@@ -3165,15 +3200,21 @@ export function createAlpRuntime(options: RuntimeOptions = {}): AlpRuntime {
|
|
|
3165
3200
|
return;
|
|
3166
3201
|
}
|
|
3167
3202
|
|
|
3168
|
-
if (
|
|
3169
|
-
|
|
3170
|
-
|
|
3171
|
-
|
|
3172
|
-
|
|
3173
|
-
) {
|
|
3174
|
-
throw new Error(
|
|
3175
|
-
|
|
3176
|
-
);
|
|
3203
|
+
if (prompt.content.some(content => content.type !== 'text' && content.type !== 'image')) {
|
|
3204
|
+
throw new Error('Only text and image prompts are supported');
|
|
3205
|
+
}
|
|
3206
|
+
// Inline images only, never a path or URL: the native input gets a data URL ALP builds itself.
|
|
3207
|
+
const images = prompt.content.filter(content => content.type === 'image') as Array<{ type: 'image'; mimeType: string; data: string }>;
|
|
3208
|
+
if (images.length) {
|
|
3209
|
+
if (!takesImages(session)) throw new Error(`${session.runtimeKind === 'acp' ? 'This ACP agent' : session.runtimeKind[0].toUpperCase() + session.runtimeKind.slice(1)} takes no images; send the message as text`);
|
|
3210
|
+
if (images.length > PROMPT_IMAGES.maxCount) throw new Error(`A prompt takes at most ${PROMPT_IMAGES.maxCount} images`);
|
|
3211
|
+
if (images.reduce((sum, image) => sum + (typeof image.data === 'string' ? base64Bytes(image.data) : 0), 0) > PROMPT_IMAGES.maxTotalBytes) throw new Error(`The images of a prompt are at most ${PROMPT_IMAGES.maxTotalBytes} bytes together`);
|
|
3212
|
+
for (const image of images) {
|
|
3213
|
+
const keys = Object.keys(image).filter(key => !['type', 'mimeType', 'data', 'name'].includes(key));
|
|
3214
|
+
if (keys.length || !PROMPT_IMAGES.mimeTypes.includes(image.mimeType) || typeof image.data !== 'string' || !/^[A-Za-z0-9+/]+={0,2}$/.test(image.data) || base64Bytes(image.data) > PROMPT_IMAGES.maxBytes) {
|
|
3215
|
+
throw new Error(`An image must be inline base64 ${PROMPT_IMAGES.mimeTypes.join(', ')} of at most ${PROMPT_IMAGES.maxBytes} bytes`);
|
|
3216
|
+
}
|
|
3217
|
+
}
|
|
3177
3218
|
}
|
|
3178
3219
|
|
|
3179
3220
|
if (
|
|
@@ -3195,9 +3236,8 @@ export function createAlpRuntime(options: RuntimeOptions = {}): AlpRuntime {
|
|
|
3195
3236
|
}
|
|
3196
3237
|
|
|
3197
3238
|
const text = prompt.content
|
|
3198
|
-
.
|
|
3199
|
-
|
|
3200
|
-
)
|
|
3239
|
+
.filter(content => content.type === 'text')
|
|
3240
|
+
.map(content => content.text ?? '')
|
|
3201
3241
|
.join('\n');
|
|
3202
3242
|
|
|
3203
3243
|
// Steering changes the brief; live assignments keep running and report by mail.
|
|
@@ -3251,6 +3291,7 @@ export function createAlpRuntime(options: RuntimeOptions = {}): AlpRuntime {
|
|
|
3251
3291
|
text: origin === 'user' ? text : promptSafe(text),
|
|
3252
3292
|
text_elements: [],
|
|
3253
3293
|
},
|
|
3294
|
+
...images.map(image => ({ type: 'image', url: `data:${image.mimeType};base64,${image.data}` })),
|
|
3254
3295
|
...(mail.length && !wake ? [{ type: 'text', text: promptSafe(renderMail(mail, session.parentAgent)), text_elements: [] }] : []),
|
|
3255
3296
|
];
|
|
3256
3297
|
|
|
@@ -3305,6 +3346,7 @@ export function createAlpRuntime(options: RuntimeOptions = {}): AlpRuntime {
|
|
|
3305
3346
|
id: `user:${prompt.clientMessageId}`,
|
|
3306
3347
|
clientMessageId: prompt.clientMessageId,
|
|
3307
3348
|
text,
|
|
3349
|
+
...(images.length ? { images: images.length } : {}),
|
|
3308
3350
|
},
|
|
3309
3351
|
});
|
|
3310
3352
|
if (origin === 'user' && !session.parent) session.userWaiting ??= Date.now();
|
|
@@ -3477,12 +3519,10 @@ export function createAlpRuntime(options: RuntimeOptions = {}): AlpRuntime {
|
|
|
3477
3519
|
|
|
3478
3520
|
async preview(spec) {
|
|
3479
3521
|
const resolveOptions = { templates: options.templates, library: options.libraryDir, language: options.language };
|
|
3480
|
-
//
|
|
3481
|
-
|
|
3482
|
-
const named = typeof settings?.workflow?.mode === 'string' && settings.workflow.mode !== 'custom' ? settings.workflow.mode : undefined;
|
|
3483
|
-
const workflow = spec.workflow ?? named ?? DEFAULT_TEAM;
|
|
3522
|
+
// The team session.create would run (resolveWorkflow, ALPD §42): the one chosen, else settings'
|
|
3523
|
+
// workflow.mode, else `custom`; a folder without settings gets init's, which name Phở.
|
|
3484
3524
|
// A preview only reads: a folder the user has not run anything in stays as it is.
|
|
3485
|
-
const mapping = await resolveSession(
|
|
3525
|
+
const mapping = await resolveSession(spec, { ...resolveOptions, initialize: false });
|
|
3486
3526
|
const teams = (await listTeams(spec.cwd, { library: options.libraryDir, templates: options.templates }))
|
|
3487
3527
|
.filter(team => !team.error)
|
|
3488
3528
|
.map(team => ({ id: team.id, label: team.label ?? team.id, ...(team.description ? { description: team.description } : {}) }));
|
package/src/session-state.ts
CHANGED
|
@@ -4,6 +4,7 @@ import type { MailEvent } from './mailbox.js';
|
|
|
4
4
|
import type { Worktree, WorktreeChange, Copy } from './workspace.js';
|
|
5
5
|
import type { Verification } from '@anhlp/alp-core/verify';
|
|
6
6
|
import type { Handoff } from './handoff.js';
|
|
7
|
+
import type { SessionUsage } from './events.js';
|
|
7
8
|
|
|
8
9
|
export type Session = {
|
|
9
10
|
runtimeKind: RuntimeKind;
|
|
@@ -52,6 +53,8 @@ export type Session = {
|
|
|
52
53
|
progressAt?: number;
|
|
53
54
|
/** A digest of the instructions the session runs with. */
|
|
54
55
|
instructionsSha?: string;
|
|
56
|
+
/** The latest usage its runtime reported, shown in viewers' context meters (ALPD §57). */
|
|
57
|
+
usage?: SessionUsage;
|
|
55
58
|
/** The context advisory was given since the context last emptied (ALPD §57). */
|
|
56
59
|
contextLevel?: number;
|
|
57
60
|
/** An assignment's brief as its requester gave it, repeated after a compaction. */
|