praxis-agent 0.35.0 → 0.37.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 +20 -2
- package/dist/application/background-agent-manager.d.ts +16 -6
- package/dist/application/background-agent-manager.js +139 -97
- package/dist/application/context-engine.d.ts +56 -0
- package/dist/application/context-engine.js +129 -0
- package/dist/application/durable-follow-up.d.ts +14 -0
- package/dist/application/durable-follow-up.js +52 -0
- package/dist/application/native-session-transcript.d.ts +72 -0
- package/dist/application/native-session-transcript.js +351 -0
- package/dist/application/native-sidechain-transcript.d.ts +34 -0
- package/dist/application/native-sidechain-transcript.js +156 -0
- package/dist/application/session-service.d.ts +23 -4
- package/dist/application/session-service.js +1130 -494
- package/dist/application/subagent-service.d.ts +5 -1
- package/dist/application/subagent-service.js +749 -320
- package/dist/application/team-agent-runtime.d.ts +29 -0
- package/dist/application/team-agent-runtime.js +169 -0
- package/dist/application/team-capability.d.ts +27 -0
- package/dist/application/team-capability.js +41 -0
- package/dist/application/team-lead-operations.d.ts +38 -0
- package/dist/application/team-lead-operations.js +155 -0
- package/dist/application/team-mailbox.d.ts +54 -0
- package/dist/application/team-mailbox.js +196 -0
- package/dist/application/team-manager.d.ts +113 -0
- package/dist/application/team-manager.js +681 -0
- package/dist/application/team-workspace.d.ts +29 -0
- package/dist/application/team-workspace.js +197 -0
- package/dist/application/top-level-agent-manager.d.ts +1 -0
- package/dist/application/top-level-agent-manager.js +1025 -403
- package/dist/application/transcript-projection.d.ts +35 -0
- package/dist/application/transcript-projection.js +139 -0
- package/dist/application/turn-lifecycle.d.ts +2 -0
- package/dist/application/turn-memory-coordinator.d.ts +43 -0
- package/dist/application/turn-memory-coordinator.js +98 -0
- package/dist/cli/agents-dashboard.js +10 -5
- package/dist/cli/interactive.d.ts +7 -0
- package/dist/cli/interactive.js +645 -334
- package/dist/cli/tui/claude-style.d.ts +8 -25
- package/dist/cli/tui/claude-style.js +299 -277
- package/dist/cli/tui/decision-surface-model.d.ts +71 -0
- package/dist/cli/tui/decision-surface-model.js +131 -0
- package/dist/cli/tui/decision-surface.d.ts +7 -0
- package/dist/cli/tui/decision-surface.js +41 -0
- package/dist/cli/tui/help-surface-model.d.ts +68 -0
- package/dist/cli/tui/help-surface-model.js +131 -0
- package/dist/cli/tui/mcp-panel.js +1 -2
- package/dist/cli/tui/permission-surface-model.d.ts +195 -0
- package/dist/cli/tui/permission-surface-model.js +360 -0
- package/dist/cli/tui/permission-surface.d.ts +7 -0
- package/dist/cli/tui/permission-surface.js +69 -0
- package/dist/cli/tui/presentation-environment.d.ts +32 -0
- package/dist/cli/tui/presentation-environment.js +149 -0
- package/dist/cli/tui/task-panel.js +10 -1
- package/dist/cli/tui/theme.d.ts +46 -3
- package/dist/cli/tui/theme.js +414 -56
- package/dist/cli/tui/tool-permission.js +16 -8
- package/dist/cli/tui/transcript-presentation.d.ts +19 -1
- package/dist/cli/tui/transcript-presentation.js +26 -6
- package/dist/cli/tui/transcript-viewport.d.ts +18 -0
- package/dist/cli/tui/transcript-viewport.js +517 -111
- package/dist/cli/tui/transcript-window-model.d.ts +108 -0
- package/dist/cli/tui/transcript-window-model.js +772 -0
- package/dist/cli/tui/tui-interaction-router.d.ts +78 -0
- package/dist/cli/tui/tui-interaction-router.js +127 -0
- package/dist/cli/tui/tui-screen-model.d.ts +114 -0
- package/dist/cli/tui/tui-screen-model.js +210 -0
- package/dist/cli/tui/tui-view-model.d.ts +2 -49
- package/dist/cli/tui/tui-view-model.js +1 -68
- package/dist/cli-runtime.d.ts +4 -2
- package/dist/cli-runtime.js +397 -32
- package/dist/compatibility/claude/transcript-codec.js +6 -0
- package/dist/core/agent-orchestration.d.ts +31 -0
- package/dist/core/agent-orchestration.js +235 -0
- package/dist/core/permission-policy.d.ts +4 -0
- package/dist/core/permission-policy.js +21 -0
- package/dist/core/runtime.d.ts +6 -0
- package/dist/core/runtime.js +20 -2
- package/dist/core/team-mailbox.d.ts +54 -0
- package/dist/core/team-mailbox.js +212 -0
- package/dist/core/team-ownership.d.ts +84 -0
- package/dist/core/team-ownership.js +505 -0
- package/dist/core/tool-execution-scheduler.js +6 -1
- package/dist/core/transcript-event.d.ts +5 -1
- package/dist/core/transcript-event.js +12 -0
- package/dist/persistence/claude-job-store.d.ts +58 -0
- package/dist/persistence/claude-job-store.js +483 -1
- package/dist/persistence/claude-transcript-store.d.ts +2 -1
- package/dist/persistence/claude-transcript-store.js +2 -10
- package/dist/persistence/native-team-mailbox.d.ts +68 -0
- package/dist/persistence/native-team-mailbox.js +542 -0
- package/dist/persistence/native-team-store.d.ts +34 -0
- package/dist/persistence/native-team-store.js +257 -0
- package/dist/persistence/native-transcript-reader.d.ts +53 -0
- package/dist/persistence/native-transcript-reader.js +283 -0
- package/dist/persistence/native-transcript-store.d.ts +69 -0
- package/dist/persistence/native-transcript-store.js +321 -0
- package/dist/persistence/subagent-lifecycle-store.d.ts +41 -8
- package/dist/persistence/subagent-lifecycle-store.js +373 -146
- package/dist/persistence/transcript-file-append.d.ts +2 -0
- package/dist/persistence/transcript-file-append.js +10 -0
- package/dist/platform/exclusive-file-lease.d.ts +3 -0
- package/dist/platform/exclusive-file-lease.js +3 -0
- package/dist/tools/claude-capabilities.d.ts +4 -0
- package/dist/tools/claude-capabilities.js +18 -2
- package/dist/tools/team-lead-tools.d.ts +19 -0
- package/dist/tools/team-lead-tools.js +479 -0
- package/dist/tools/team-member-tools.d.ts +21 -0
- package/dist/tools/team-member-tools.js +259 -0
- package/package.json +1 -1
- package/dist/cli/tui/fullscreen-renderer.d.ts +0 -14
- package/dist/cli/tui/fullscreen-renderer.js +0 -13
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import { AgentRunCancelledError, } from '../core/runtime.js';
|
|
2
|
+
import { contextRecoveryMadeProgress, isPromptTooLongError, } from '../core/context-budget.js';
|
|
3
|
+
function assertSignal(signal) {
|
|
4
|
+
if (signal?.aborted)
|
|
5
|
+
throw new AgentRunCancelledError();
|
|
6
|
+
}
|
|
7
|
+
function validateEnvelope(envelope) {
|
|
8
|
+
if (!Array.isArray(envelope.messages) || !Array.isArray(envelope.tools)) {
|
|
9
|
+
throw new TypeError('Context envelope must contain messages and tools arrays');
|
|
10
|
+
}
|
|
11
|
+
if (envelope.outputTokens !== undefined &&
|
|
12
|
+
(!Number.isSafeInteger(envelope.outputTokens) || envelope.outputTokens < 0)) {
|
|
13
|
+
throw new TypeError('Context envelope outputTokens must be nonnegative');
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
/** Owns context measurement, bounded recovery, and the proposal/commit seam. */
|
|
17
|
+
export class ContextEngine {
|
|
18
|
+
recoveryUsed = false;
|
|
19
|
+
budget;
|
|
20
|
+
memory;
|
|
21
|
+
autoCompact;
|
|
22
|
+
constructor(options = {}) {
|
|
23
|
+
this.budget = options.budget;
|
|
24
|
+
this.memory = options.memory ?? {};
|
|
25
|
+
this.autoCompact = options.autoCompact ?? true;
|
|
26
|
+
}
|
|
27
|
+
async prepare(port, signal) {
|
|
28
|
+
const current = port.current();
|
|
29
|
+
validateEnvelope(current);
|
|
30
|
+
if (!this.budget || !this.autoCompact)
|
|
31
|
+
return current;
|
|
32
|
+
const before = this.reportRequired(current);
|
|
33
|
+
if (!before.shouldCompact)
|
|
34
|
+
return current;
|
|
35
|
+
assertSignal(signal);
|
|
36
|
+
const irreducible = port.irreducible();
|
|
37
|
+
validateEnvelope(irreducible);
|
|
38
|
+
const irreducibleReport = this.reportRequired(irreducible);
|
|
39
|
+
this.budget.assertFits(irreducibleReport);
|
|
40
|
+
await this.memory.beforeCompact?.();
|
|
41
|
+
assertSignal(signal);
|
|
42
|
+
const proposal = await port.propose({
|
|
43
|
+
trigger: 'auto',
|
|
44
|
+
before,
|
|
45
|
+
irreducible: irreducibleReport,
|
|
46
|
+
...(signal ? { signal } : {}),
|
|
47
|
+
});
|
|
48
|
+
assertSignal(signal);
|
|
49
|
+
validateEnvelope(proposal.envelope);
|
|
50
|
+
this.budget.assertFits(this.reportRequired(proposal.envelope));
|
|
51
|
+
await proposal.commit();
|
|
52
|
+
await this.memory.afterCompact?.();
|
|
53
|
+
return proposal.envelope;
|
|
54
|
+
}
|
|
55
|
+
async recover(error, port, signal) {
|
|
56
|
+
if (!isPromptTooLongError(error))
|
|
57
|
+
return { kind: 'not-applicable' };
|
|
58
|
+
if (this.recoveryUsed)
|
|
59
|
+
return { kind: 'exhausted', error };
|
|
60
|
+
// Recovery is a single turn-scoped opportunity, regardless of whether the
|
|
61
|
+
// proposal eventually proves usable. This prevents provider retry loops.
|
|
62
|
+
this.recoveryUsed = true;
|
|
63
|
+
assertSignal(signal);
|
|
64
|
+
if (!this.budget || !this.autoCompact) {
|
|
65
|
+
return { kind: 'exhausted', error };
|
|
66
|
+
}
|
|
67
|
+
try {
|
|
68
|
+
const current = port.current();
|
|
69
|
+
validateEnvelope(current);
|
|
70
|
+
const before = this.reportRequired(current, { promptTooLong: true });
|
|
71
|
+
const irreducible = port.irreducible();
|
|
72
|
+
validateEnvelope(irreducible);
|
|
73
|
+
const irreducibleReport = this.reportRequired(irreducible);
|
|
74
|
+
this.budget.assertFits(irreducibleReport);
|
|
75
|
+
await this.memory.beforeCompact?.();
|
|
76
|
+
assertSignal(signal);
|
|
77
|
+
const proposal = await port.propose({
|
|
78
|
+
trigger: 'reactive',
|
|
79
|
+
before,
|
|
80
|
+
irreducible: irreducibleReport,
|
|
81
|
+
...(signal ? { signal } : {}),
|
|
82
|
+
});
|
|
83
|
+
assertSignal(signal);
|
|
84
|
+
validateEnvelope(proposal.envelope);
|
|
85
|
+
const after = this.reportRequired(proposal.envelope);
|
|
86
|
+
this.budget.assertFits(after);
|
|
87
|
+
if (!contextRecoveryMadeProgress({
|
|
88
|
+
beforeOccupancyTokens: before.occupancyTokens,
|
|
89
|
+
afterOccupancyTokens: after.occupancyTokens,
|
|
90
|
+
})) {
|
|
91
|
+
return { kind: 'exhausted', error };
|
|
92
|
+
}
|
|
93
|
+
await proposal.commit();
|
|
94
|
+
await this.memory.afterCompact?.();
|
|
95
|
+
return { kind: 'retry', envelope: proposal.envelope };
|
|
96
|
+
}
|
|
97
|
+
catch (cause) {
|
|
98
|
+
if (signal?.aborted || cause instanceof AgentRunCancelledError)
|
|
99
|
+
throw cause;
|
|
100
|
+
return { kind: 'exhausted', error };
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
observeUsage(usage, messages, tools) {
|
|
104
|
+
this.budget?.observeUsage(usage, messages, tools);
|
|
105
|
+
}
|
|
106
|
+
report(envelope, options = {}) {
|
|
107
|
+
validateEnvelope(envelope);
|
|
108
|
+
if (!this.budget) {
|
|
109
|
+
return undefined;
|
|
110
|
+
}
|
|
111
|
+
return this.budget.evaluate(envelope.messages, envelope.tools, {
|
|
112
|
+
...(envelope.outputTokens === undefined
|
|
113
|
+
? {}
|
|
114
|
+
: { outputTokens: envelope.outputTokens }),
|
|
115
|
+
...options,
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
reportRequired(envelope, options = {}) {
|
|
119
|
+
if (!this.budget)
|
|
120
|
+
throw new Error('Context budget is unavailable');
|
|
121
|
+
return this.budget.evaluate(envelope.messages, envelope.tools, {
|
|
122
|
+
...(envelope.outputTokens === undefined
|
|
123
|
+
? {}
|
|
124
|
+
: { outputTokens: envelope.outputTokens }),
|
|
125
|
+
...options,
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
//# sourceMappingURL=context-engine.js.map
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export interface DurableFollowUpBatch {
|
|
2
|
+
readonly id: string;
|
|
3
|
+
readonly messages: readonly string[];
|
|
4
|
+
acknowledge(): Promise<void>;
|
|
5
|
+
}
|
|
6
|
+
export declare class DurableFollowUpTracker {
|
|
7
|
+
private readonly pending;
|
|
8
|
+
register(batch: DurableFollowUpBatch): void;
|
|
9
|
+
add(batch: DurableFollowUpBatch): void;
|
|
10
|
+
track(batch: DurableFollowUpBatch): void;
|
|
11
|
+
followUpUserMessagesCompleted(messages: readonly string[]): Promise<void>;
|
|
12
|
+
size(): number;
|
|
13
|
+
}
|
|
14
|
+
//# sourceMappingURL=durable-follow-up.d.ts.map
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
function multiset(values) {
|
|
2
|
+
const result = new Map();
|
|
3
|
+
for (const value of values)
|
|
4
|
+
result.set(value, (result.get(value) ?? 0) + 1);
|
|
5
|
+
return result;
|
|
6
|
+
}
|
|
7
|
+
function containsAll(container, required) {
|
|
8
|
+
const available = multiset(container);
|
|
9
|
+
for (const [value, count] of multiset(required)) {
|
|
10
|
+
if ((available.get(value) ?? 0) < count)
|
|
11
|
+
return false;
|
|
12
|
+
}
|
|
13
|
+
return true;
|
|
14
|
+
}
|
|
15
|
+
export class DurableFollowUpTracker {
|
|
16
|
+
pending = new Map();
|
|
17
|
+
register(batch) {
|
|
18
|
+
if (!batch || typeof batch.id !== 'string' || batch.id.trim() === '')
|
|
19
|
+
throw new Error('Invalid durable follow-up batch');
|
|
20
|
+
if (!Array.isArray(batch.messages) || batch.messages.length === 0)
|
|
21
|
+
return;
|
|
22
|
+
if (batch.messages.some((message) => typeof message !== 'string'))
|
|
23
|
+
throw new Error('Invalid durable follow-up messages');
|
|
24
|
+
const existing = this.pending.get(batch.id);
|
|
25
|
+
if (existing) {
|
|
26
|
+
if (existing.messages.length !== batch.messages.length ||
|
|
27
|
+
existing.messages.some((message, index) => message !== batch.messages[index]))
|
|
28
|
+
throw new Error(`Conflicting durable follow-up batch: ${batch.id}`);
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
this.pending.set(batch.id, batch);
|
|
32
|
+
}
|
|
33
|
+
add(batch) {
|
|
34
|
+
this.register(batch);
|
|
35
|
+
}
|
|
36
|
+
track(batch) {
|
|
37
|
+
this.register(batch);
|
|
38
|
+
}
|
|
39
|
+
async followUpUserMessagesCompleted(messages) {
|
|
40
|
+
const completed = [...messages];
|
|
41
|
+
for (const [id, batch] of this.pending) {
|
|
42
|
+
if (!containsAll(completed, batch.messages))
|
|
43
|
+
continue;
|
|
44
|
+
await batch.acknowledge();
|
|
45
|
+
this.pending.delete(id);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
size() {
|
|
49
|
+
return this.pending.size;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
//# sourceMappingURL=durable-follow-up.js.map
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import type { ModelMessage, ModelToolCall, ToolExecutionResult } from '../core/runtime.js';
|
|
2
|
+
import type { TranscriptMessagesEvent } from '../core/transcript-event.js';
|
|
3
|
+
import type { NativeTranscriptStore } from '../persistence/native-transcript-store.js';
|
|
4
|
+
export type NativeTranscriptActivation = {
|
|
5
|
+
readonly kind: 'start';
|
|
6
|
+
} | {
|
|
7
|
+
readonly kind: 'resume';
|
|
8
|
+
readonly atEventId?: string;
|
|
9
|
+
};
|
|
10
|
+
export interface NativeMessageAppend {
|
|
11
|
+
readonly messages: readonly ModelMessage[];
|
|
12
|
+
readonly model?: string;
|
|
13
|
+
readonly terminalReason?: TranscriptMessagesEvent['terminalReason'];
|
|
14
|
+
}
|
|
15
|
+
export interface NativeCompactionAppend {
|
|
16
|
+
readonly summary: string;
|
|
17
|
+
readonly trigger: 'auto' | 'manual';
|
|
18
|
+
readonly preTokens: number;
|
|
19
|
+
readonly postTokens: number;
|
|
20
|
+
readonly durationMs: number;
|
|
21
|
+
readonly preservedMessages?: readonly ModelMessage[];
|
|
22
|
+
}
|
|
23
|
+
export type NativeInterruption = {
|
|
24
|
+
readonly kind: 'complete' | 'none';
|
|
25
|
+
} | {
|
|
26
|
+
readonly kind: 'interrupted-prompt';
|
|
27
|
+
readonly prompt: string;
|
|
28
|
+
} | {
|
|
29
|
+
readonly kind: 'interrupted-turn';
|
|
30
|
+
} | {
|
|
31
|
+
readonly kind: 'recoverable-tools';
|
|
32
|
+
readonly calls: readonly ModelToolCall[];
|
|
33
|
+
} | {
|
|
34
|
+
readonly kind: 'indeterminate-tools';
|
|
35
|
+
readonly callIds: readonly string[];
|
|
36
|
+
};
|
|
37
|
+
export interface NativeSessionTranscriptLease {
|
|
38
|
+
activeMessages(): ModelMessage[];
|
|
39
|
+
interruption(): NativeInterruption;
|
|
40
|
+
beginToolExecution(callId: string): Promise<void>;
|
|
41
|
+
appendToolCompletion(input: {
|
|
42
|
+
callId: string;
|
|
43
|
+
result: ToolExecutionResult;
|
|
44
|
+
followUpUserMessages?: readonly string[];
|
|
45
|
+
}): Promise<void>;
|
|
46
|
+
appendMessages(input: NativeMessageAppend): Promise<string>;
|
|
47
|
+
appendCompaction(input: NativeCompactionAppend): Promise<{
|
|
48
|
+
boundaryId: string;
|
|
49
|
+
summaryId: string;
|
|
50
|
+
}>;
|
|
51
|
+
}
|
|
52
|
+
export interface NativeForkOptions {
|
|
53
|
+
readonly atEventId?: string;
|
|
54
|
+
readonly recordCount?: number;
|
|
55
|
+
readonly ensureExisting?: boolean;
|
|
56
|
+
}
|
|
57
|
+
export interface NativeSessionTranscriptOptions {
|
|
58
|
+
readonly sessionId: string;
|
|
59
|
+
readonly store: NativeTranscriptStore;
|
|
60
|
+
readonly createId?: () => string;
|
|
61
|
+
readonly now?: () => string;
|
|
62
|
+
}
|
|
63
|
+
export declare class NativeSessionTranscript {
|
|
64
|
+
private readonly sessionId;
|
|
65
|
+
private readonly store;
|
|
66
|
+
private readonly createId;
|
|
67
|
+
private readonly now;
|
|
68
|
+
constructor(options: NativeSessionTranscriptOptions);
|
|
69
|
+
forkTo(target: NativeSessionTranscript, options?: NativeForkOptions): Promise<void>;
|
|
70
|
+
withLease<T>(activation: NativeTranscriptActivation, operation: (lease: NativeSessionTranscriptLease) => Promise<T>): Promise<T>;
|
|
71
|
+
}
|
|
72
|
+
//# sourceMappingURL=native-session-transcript.d.ts.map
|
|
@@ -0,0 +1,351 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import { activeEvents, projectActiveMessages, unresolvedActiveToolCallIds, } from './transcript-projection.js';
|
|
3
|
+
export class NativeSessionTranscript {
|
|
4
|
+
sessionId;
|
|
5
|
+
store;
|
|
6
|
+
createId;
|
|
7
|
+
now;
|
|
8
|
+
constructor(options) {
|
|
9
|
+
this.sessionId = options.sessionId;
|
|
10
|
+
this.store = options.store;
|
|
11
|
+
this.createId = options.createId ?? randomUUID;
|
|
12
|
+
this.now = options.now ?? (() => new Date().toISOString());
|
|
13
|
+
}
|
|
14
|
+
async forkTo(target, options = {}) {
|
|
15
|
+
if (this.sessionId === target.sessionId)
|
|
16
|
+
throw new Error('native fork source and target session IDs must differ');
|
|
17
|
+
const sourceResult = await this.store.withLease((lease) => lease.load());
|
|
18
|
+
if (sourceResult.status === 'conflict')
|
|
19
|
+
throw new Error(`native fork source lease conflict: ${sourceResult.reason}`);
|
|
20
|
+
const source = sourceResult.value;
|
|
21
|
+
if (source.records.length === 0)
|
|
22
|
+
throw new Error('native fork source session is missing or empty');
|
|
23
|
+
if (source.records.some((record) => record.event.sessionId !== this.sessionId))
|
|
24
|
+
throw new Error('native fork source sessionId does not match');
|
|
25
|
+
const recordCount = options.recordCount;
|
|
26
|
+
if (recordCount !== undefined &&
|
|
27
|
+
(!Number.isSafeInteger(recordCount) ||
|
|
28
|
+
recordCount <= 0 ||
|
|
29
|
+
recordCount > source.records.length))
|
|
30
|
+
throw new Error('native fork source recordCount is invalid');
|
|
31
|
+
const prefix = source.records.slice(0, recordCount);
|
|
32
|
+
const selected = activeEvents(prefix.map((record) => record.event), options.atEventId);
|
|
33
|
+
const terminal = prefix.at(-1)?.event;
|
|
34
|
+
if (terminal?.kind === 'context-boundary' ||
|
|
35
|
+
selected.at(-1)?.kind === 'context-boundary')
|
|
36
|
+
throw new Error('native fork source ends with an incomplete context boundary');
|
|
37
|
+
const expected = selected.map((event) => ({
|
|
38
|
+
...event,
|
|
39
|
+
sessionId: target.sessionId,
|
|
40
|
+
}));
|
|
41
|
+
const ensureExisting = options.ensureExisting === true;
|
|
42
|
+
const targetResult = await target.store.withLease(async (lease) => {
|
|
43
|
+
const reservation = await lease.reserve();
|
|
44
|
+
if (reservation.status === 'reserved') {
|
|
45
|
+
const empty = await lease.load();
|
|
46
|
+
const appended = await lease.appendMany(empty.tail, expected);
|
|
47
|
+
if (appended.status === 'conflict')
|
|
48
|
+
throw new Error(`native fork target append conflict: ${appended.reason}`);
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
if (!ensureExisting)
|
|
52
|
+
throw new Error('native fork target already exists');
|
|
53
|
+
const existing = await lease.load();
|
|
54
|
+
if (existing.records.length < expected.length ||
|
|
55
|
+
expected.some((event, index) => JSON.stringify(existing.records[index]?.event) !==
|
|
56
|
+
JSON.stringify(event)))
|
|
57
|
+
throw new Error('native fork target is not the expected native fork');
|
|
58
|
+
});
|
|
59
|
+
if (targetResult.status === 'conflict')
|
|
60
|
+
throw new Error(`native fork target lease conflict: ${targetResult.reason}`);
|
|
61
|
+
}
|
|
62
|
+
async withLease(activation, operation) {
|
|
63
|
+
let prepared;
|
|
64
|
+
let selectedId;
|
|
65
|
+
const result = await this.store.withLease(async (nativeLease) => {
|
|
66
|
+
if (activation.kind === 'start') {
|
|
67
|
+
const reserved = await nativeLease.reserve();
|
|
68
|
+
if (reserved.status === 'conflict')
|
|
69
|
+
throw new Error('native transcript session already exists');
|
|
70
|
+
}
|
|
71
|
+
prepared = await nativeLease.load();
|
|
72
|
+
if (activation.kind === 'resume') {
|
|
73
|
+
if (prepared.records.length === 0)
|
|
74
|
+
throw new Error('native transcript session is missing or empty');
|
|
75
|
+
if (prepared.records.some((record) => record.event.sessionId !== this.sessionId))
|
|
76
|
+
throw new Error('native transcript sessionId does not match');
|
|
77
|
+
const events = prepared.records.map((record) => record.event);
|
|
78
|
+
const selected = activeEvents(events, activation.atEventId);
|
|
79
|
+
if (selected.length === 0)
|
|
80
|
+
throw new Error('native transcript session has no active events');
|
|
81
|
+
selectedId = selected.at(-1)?.id;
|
|
82
|
+
}
|
|
83
|
+
const initial = prepared;
|
|
84
|
+
const currentId = selectedId ?? initial.records.at(-1)?.event.id ?? null;
|
|
85
|
+
const records = [...initial.records];
|
|
86
|
+
let tail = currentId === null
|
|
87
|
+
? initial.tail
|
|
88
|
+
: { ...initial.tail, branchParentId: currentId };
|
|
89
|
+
let activeId = currentId;
|
|
90
|
+
let mutationQueue = Promise.resolve();
|
|
91
|
+
const enqueue = (operation) => {
|
|
92
|
+
const result = mutationQueue.then(operation, operation);
|
|
93
|
+
mutationQueue = result.then(() => undefined, () => undefined);
|
|
94
|
+
return result;
|
|
95
|
+
};
|
|
96
|
+
const lease = {
|
|
97
|
+
activeMessages: () => projectActiveMessages(records.map((record) => record.event), activeId ?? undefined).slice(),
|
|
98
|
+
interruption: () => {
|
|
99
|
+
const events = activeEvents(records.map((record) => record.event), activeId ?? undefined);
|
|
100
|
+
const boundaryIndex = Math.max(-1, ...events
|
|
101
|
+
.map((event, index) => event.kind === 'context-boundary' ? index : -1)
|
|
102
|
+
.filter((index) => index >= 0));
|
|
103
|
+
const active = events.slice(boundaryIndex + 1);
|
|
104
|
+
const calls = new Map();
|
|
105
|
+
const results = new Set();
|
|
106
|
+
const claimed = new Set();
|
|
107
|
+
for (const event of active) {
|
|
108
|
+
if (event.kind === 'tool-execution-started') {
|
|
109
|
+
claimed.add(event.callId);
|
|
110
|
+
continue;
|
|
111
|
+
}
|
|
112
|
+
if (event.kind !== 'messages')
|
|
113
|
+
continue;
|
|
114
|
+
for (const message of event.messages) {
|
|
115
|
+
if (message.role === 'assistant')
|
|
116
|
+
for (const call of message.toolCalls ?? [])
|
|
117
|
+
calls.set(call.id, call);
|
|
118
|
+
else if (message.role === 'tool')
|
|
119
|
+
results.add(message.toolCallId);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
const unresolved = [...calls.entries()].filter(([id]) => !results.has(id));
|
|
123
|
+
const indeterminate = unresolved
|
|
124
|
+
.map(([id]) => id)
|
|
125
|
+
.filter((id) => claimed.has(id));
|
|
126
|
+
if (indeterminate.length > 0)
|
|
127
|
+
return { kind: 'indeterminate-tools', callIds: indeterminate };
|
|
128
|
+
if (unresolved.length > 0)
|
|
129
|
+
return {
|
|
130
|
+
kind: 'recoverable-tools',
|
|
131
|
+
calls: unresolved.map(([, call]) => call),
|
|
132
|
+
};
|
|
133
|
+
let lastMessages;
|
|
134
|
+
for (const event of active) {
|
|
135
|
+
if (event.kind !== 'messages')
|
|
136
|
+
continue;
|
|
137
|
+
lastMessages = event.messages;
|
|
138
|
+
}
|
|
139
|
+
const lastMessage = lastMessages?.at(-1);
|
|
140
|
+
if (lastMessage?.role === 'assistant')
|
|
141
|
+
return { kind: 'complete' };
|
|
142
|
+
if (lastMessages?.some((message) => message.role === 'tool'))
|
|
143
|
+
return { kind: 'interrupted-turn' };
|
|
144
|
+
if (lastMessage?.role === 'user')
|
|
145
|
+
return { kind: 'interrupted-prompt', prompt: lastMessage.content };
|
|
146
|
+
return { kind: 'none' };
|
|
147
|
+
},
|
|
148
|
+
beginToolExecution: (callId) => enqueue(async () => {
|
|
149
|
+
const events = activeEvents(records.map((record) => record.event), activeId ?? undefined);
|
|
150
|
+
const activeMessages = projectActiveMessages(records.map((record) => record.event), activeId ?? undefined);
|
|
151
|
+
const call = activeMessages
|
|
152
|
+
.filter((message) => message.role === 'assistant')
|
|
153
|
+
.flatMap((message) => message.toolCalls ?? [])
|
|
154
|
+
.find((candidate) => candidate.id === callId);
|
|
155
|
+
if (!call)
|
|
156
|
+
throw new Error(`Unknown native tool call: ${callId}`);
|
|
157
|
+
const boundaryIndex = Math.max(-1, ...events.map((event, index) => event.kind === 'context-boundary' ? index : -1));
|
|
158
|
+
const postBoundary = events.slice(boundaryIndex + 1);
|
|
159
|
+
const resolved = new Set(projectActiveMessages(records.map((record) => record.event), activeId ?? undefined)
|
|
160
|
+
.filter((message) => message.role === 'tool')
|
|
161
|
+
.map((message) => message.toolCallId));
|
|
162
|
+
if (resolved.has(callId))
|
|
163
|
+
throw new Error(`Native tool call is already resolved: ${callId}`);
|
|
164
|
+
if (postBoundary.some((event) => event.kind === 'tool-execution-started' &&
|
|
165
|
+
event.callId === callId))
|
|
166
|
+
throw new Error(`Native tool call already claimed: ${callId}`);
|
|
167
|
+
const event = {
|
|
168
|
+
kind: 'tool-execution-started',
|
|
169
|
+
id: this.createId(),
|
|
170
|
+
parentId: activeId,
|
|
171
|
+
sessionId: this.sessionId,
|
|
172
|
+
timestamp: this.now(),
|
|
173
|
+
callId,
|
|
174
|
+
};
|
|
175
|
+
const appended = await nativeLease.append(tail, event);
|
|
176
|
+
if (appended.status === 'conflict')
|
|
177
|
+
throw new Error(`native transcript append conflict: ${appended.reason}`);
|
|
178
|
+
records.push({ event });
|
|
179
|
+
tail = appended.tail;
|
|
180
|
+
activeId = event.id;
|
|
181
|
+
}),
|
|
182
|
+
appendToolCompletion: (input) => enqueue(async () => {
|
|
183
|
+
const { callId, result, followUpUserMessages = [] } = input;
|
|
184
|
+
const events = activeEvents(records.map((record) => record.event), activeId ?? undefined);
|
|
185
|
+
const messages = projectActiveMessages(records.map((record) => record.event), activeId ?? undefined);
|
|
186
|
+
const call = messages
|
|
187
|
+
.filter((message) => message.role === 'assistant')
|
|
188
|
+
.flatMap((message) => message.toolCalls ?? [])
|
|
189
|
+
.find((candidate) => candidate.id === callId);
|
|
190
|
+
if (!call)
|
|
191
|
+
throw new Error(`Unknown native tool call: ${callId}`);
|
|
192
|
+
if (!events.some((event) => event.kind === 'tool-execution-started' &&
|
|
193
|
+
event.callId === callId))
|
|
194
|
+
throw new Error(`Native tool call was not claimed: ${callId}`);
|
|
195
|
+
if (messages.some((message) => message.role === 'tool' && message.toolCallId === callId))
|
|
196
|
+
throw new Error(`Native tool call is already resolved: ${callId}`);
|
|
197
|
+
const event = {
|
|
198
|
+
kind: 'messages',
|
|
199
|
+
id: this.createId(),
|
|
200
|
+
parentId: activeId,
|
|
201
|
+
sessionId: this.sessionId,
|
|
202
|
+
timestamp: this.now(),
|
|
203
|
+
messages: [
|
|
204
|
+
{
|
|
205
|
+
role: 'tool',
|
|
206
|
+
toolCallId: callId,
|
|
207
|
+
content: result.content,
|
|
208
|
+
...(result.contentBlocks?.length
|
|
209
|
+
? { contentBlocks: result.contentBlocks }
|
|
210
|
+
: {}),
|
|
211
|
+
...(result.images?.length ? { images: result.images } : {}),
|
|
212
|
+
...(result.documents?.length
|
|
213
|
+
? { documents: result.documents }
|
|
214
|
+
: {}),
|
|
215
|
+
isError: result.isError,
|
|
216
|
+
},
|
|
217
|
+
...followUpUserMessages.map((content) => ({
|
|
218
|
+
role: 'user',
|
|
219
|
+
content,
|
|
220
|
+
})),
|
|
221
|
+
],
|
|
222
|
+
};
|
|
223
|
+
const appended = await nativeLease.append(tail, event);
|
|
224
|
+
if (appended.status === 'conflict')
|
|
225
|
+
throw new Error(`native transcript append conflict: ${appended.reason}`);
|
|
226
|
+
records.push({ event });
|
|
227
|
+
tail = appended.tail;
|
|
228
|
+
activeId = event.id;
|
|
229
|
+
}),
|
|
230
|
+
appendMessages: (input) => enqueue(async () => {
|
|
231
|
+
if (input.messages.length === 0)
|
|
232
|
+
throw new Error('native transcript cannot append empty messages');
|
|
233
|
+
const event = {
|
|
234
|
+
kind: 'messages',
|
|
235
|
+
id: this.createId(),
|
|
236
|
+
parentId: activeId,
|
|
237
|
+
sessionId: this.sessionId,
|
|
238
|
+
timestamp: this.now(),
|
|
239
|
+
messages: [...input.messages],
|
|
240
|
+
...(input.model === undefined ? {} : { model: input.model }),
|
|
241
|
+
...(input.terminalReason === undefined
|
|
242
|
+
? {}
|
|
243
|
+
: { terminalReason: input.terminalReason }),
|
|
244
|
+
};
|
|
245
|
+
const appended = await nativeLease.append(tail, event);
|
|
246
|
+
if (appended.status === 'conflict')
|
|
247
|
+
throw new Error(`native transcript append conflict: ${appended.reason}`);
|
|
248
|
+
records.push({ event });
|
|
249
|
+
tail = appended.tail;
|
|
250
|
+
activeId = event.id;
|
|
251
|
+
return event.id;
|
|
252
|
+
}),
|
|
253
|
+
appendCompaction: (input) => enqueue(async () => {
|
|
254
|
+
if (records.length === 0 || activeId === null)
|
|
255
|
+
throw new Error('Cannot compact an empty native transcript');
|
|
256
|
+
if (input.summary.trim().length === 0)
|
|
257
|
+
throw new Error('Native compact summary must not be blank');
|
|
258
|
+
for (const value of [
|
|
259
|
+
input.preTokens,
|
|
260
|
+
input.postTokens,
|
|
261
|
+
input.durationMs,
|
|
262
|
+
]) {
|
|
263
|
+
if (!Number.isFinite(value) || value < 0)
|
|
264
|
+
throw new Error('Native compact accounting values must be non-negative');
|
|
265
|
+
}
|
|
266
|
+
const events = records.map((record) => record.event);
|
|
267
|
+
if (unresolvedActiveToolCallIds(projectActiveMessages(events, activeId)).length > 0)
|
|
268
|
+
throw new Error('Cannot compact a native transcript with unresolved tool calls');
|
|
269
|
+
const boundaryId = this.createId();
|
|
270
|
+
const summaryId = this.createId();
|
|
271
|
+
const boundary = {
|
|
272
|
+
kind: 'context-boundary',
|
|
273
|
+
id: boundaryId,
|
|
274
|
+
parentId: null,
|
|
275
|
+
sessionId: this.sessionId,
|
|
276
|
+
timestamp: this.now(),
|
|
277
|
+
logicalParentId: activeId,
|
|
278
|
+
trigger: input.trigger,
|
|
279
|
+
preTokens: input.preTokens,
|
|
280
|
+
postTokens: input.postTokens,
|
|
281
|
+
durationMs: input.durationMs,
|
|
282
|
+
};
|
|
283
|
+
const summary = {
|
|
284
|
+
kind: 'context-summary',
|
|
285
|
+
id: summaryId,
|
|
286
|
+
parentId: boundaryId,
|
|
287
|
+
sessionId: this.sessionId,
|
|
288
|
+
timestamp: this.now(),
|
|
289
|
+
summary: input.summary,
|
|
290
|
+
};
|
|
291
|
+
const suffix = input.preservedMessages ?? [];
|
|
292
|
+
const replayToolCallIds = new Map();
|
|
293
|
+
for (const message of suffix) {
|
|
294
|
+
if (message.role !== 'assistant')
|
|
295
|
+
continue;
|
|
296
|
+
for (const call of message.toolCalls ?? [])
|
|
297
|
+
replayToolCallIds.set(call.id, this.createId());
|
|
298
|
+
}
|
|
299
|
+
const replaySuffix = suffix.map((message) => {
|
|
300
|
+
if (message.role === 'assistant')
|
|
301
|
+
return {
|
|
302
|
+
...message,
|
|
303
|
+
...(message.toolCalls === undefined
|
|
304
|
+
? {}
|
|
305
|
+
: {
|
|
306
|
+
toolCalls: message.toolCalls.map((call) => ({
|
|
307
|
+
...call,
|
|
308
|
+
id: replayToolCallIds.get(call.id) ?? call.id,
|
|
309
|
+
})),
|
|
310
|
+
}),
|
|
311
|
+
};
|
|
312
|
+
if (message.role === 'tool')
|
|
313
|
+
return {
|
|
314
|
+
...message,
|
|
315
|
+
toolCallId: replayToolCallIds.get(message.toolCallId) ??
|
|
316
|
+
message.toolCallId,
|
|
317
|
+
};
|
|
318
|
+
return { ...message };
|
|
319
|
+
});
|
|
320
|
+
const suffixEvent = replaySuffix.length
|
|
321
|
+
? {
|
|
322
|
+
kind: 'messages',
|
|
323
|
+
id: this.createId(),
|
|
324
|
+
parentId: summaryId,
|
|
325
|
+
sessionId: this.sessionId,
|
|
326
|
+
timestamp: this.now(),
|
|
327
|
+
messages: replaySuffix,
|
|
328
|
+
}
|
|
329
|
+
: undefined;
|
|
330
|
+
const compactedEvents = [
|
|
331
|
+
boundary,
|
|
332
|
+
summary,
|
|
333
|
+
...(suffixEvent === undefined ? [] : [suffixEvent]),
|
|
334
|
+
];
|
|
335
|
+
const appended = await nativeLease.appendMany(tail, compactedEvents);
|
|
336
|
+
if (appended.status === 'conflict')
|
|
337
|
+
throw new Error(`native transcript append conflict: ${appended.reason}`);
|
|
338
|
+
records.push({ event: boundary }, { event: summary }, ...(suffixEvent === undefined ? [] : [{ event: suffixEvent }]));
|
|
339
|
+
tail = appended.tail;
|
|
340
|
+
activeId = suffixEvent?.id ?? summaryId;
|
|
341
|
+
return { boundaryId, summaryId };
|
|
342
|
+
}),
|
|
343
|
+
};
|
|
344
|
+
return operation(lease);
|
|
345
|
+
});
|
|
346
|
+
if (result.status === 'conflict')
|
|
347
|
+
throw new Error(`native transcript lease conflict: ${result.reason}`);
|
|
348
|
+
return result.value;
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
//# sourceMappingURL=native-session-transcript.js.map
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { type NativeSessionTranscriptLease } from './native-session-transcript.js';
|
|
2
|
+
export type NativeSidechainPermissionMode = 'acceptEdits' | 'auto' | 'bypassPermissions' | 'default' | 'dontAsk' | 'plan';
|
|
3
|
+
export interface NativeSidechainMetadata {
|
|
4
|
+
readonly agentType: string;
|
|
5
|
+
readonly description: string;
|
|
6
|
+
readonly toolUseId: string;
|
|
7
|
+
readonly spawnDepth: number;
|
|
8
|
+
readonly cwd: string;
|
|
9
|
+
readonly promptId: string;
|
|
10
|
+
readonly name?: string;
|
|
11
|
+
readonly permissionMode?: NativeSidechainPermissionMode;
|
|
12
|
+
readonly isolation?: 'worktree';
|
|
13
|
+
readonly parentAgentId?: string;
|
|
14
|
+
readonly worktreePath?: string;
|
|
15
|
+
}
|
|
16
|
+
export interface NativeSidechainPaths {
|
|
17
|
+
readonly sessionId: string;
|
|
18
|
+
readonly agentId: string;
|
|
19
|
+
readonly directory: string;
|
|
20
|
+
readonly transcriptFile: string;
|
|
21
|
+
readonly metadataFile: string;
|
|
22
|
+
readonly lockFile?: string;
|
|
23
|
+
}
|
|
24
|
+
export declare class NativeSidechainTranscript {
|
|
25
|
+
private readonly paths;
|
|
26
|
+
private readonly transcript;
|
|
27
|
+
private readonly store;
|
|
28
|
+
constructor(paths: NativeSidechainPaths);
|
|
29
|
+
create(prompt: string, metadata: NativeSidechainMetadata): Promise<void>;
|
|
30
|
+
withLease<T>(operation: (lease: NativeSessionTranscriptLease) => Promise<T>): Promise<T>;
|
|
31
|
+
loadReadOnly(): Promise<import("../persistence/native-transcript-store.js").NativeTranscriptRecovery>;
|
|
32
|
+
metadata(): Promise<NativeSidechainMetadata>;
|
|
33
|
+
}
|
|
34
|
+
//# sourceMappingURL=native-sidechain-transcript.d.ts.map
|