praxis-agent 0.45.4 → 0.46.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 +4 -1
- package/dist/application/session-service.d.ts +4 -0
- package/dist/application/session-service.js +83 -0
- package/dist/cli/interactive.d.ts +3 -0
- package/dist/cli/interactive.js +222 -4
- package/dist/cli/tui/quiet-frame.d.ts +6 -1
- package/dist/cli/tui/quiet-frame.js +4 -0
- package/dist/cli/tui/quiet-screen-projector.d.ts +1 -0
- package/dist/cli/tui/quiet-screen-projector.js +3 -0
- package/dist/cli/tui/tui-interaction-router.js +1 -1
- package/dist/core/active-turn-input.d.ts +63 -0
- package/dist/core/active-turn-input.js +53 -0
- package/dist/core/runtime.d.ts +12 -0
- package/dist/core/runtime.js +66 -7
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -106,7 +106,10 @@ troubleshooting. Run `praxis --help` for the authoritative command surface.
|
|
|
106
106
|
terminal-native background, and a minimal composer/status row. Interactive
|
|
107
107
|
surfaces share the same presentation across terminals, with English
|
|
108
108
|
permission/configuration choices and a taught `❯` / Up/Down / Enter / Esc
|
|
109
|
-
interaction grammar,
|
|
109
|
+
interaction grammar. While a regular turn is active, the composer remains
|
|
110
|
+
editable: Enter steers at the next safe continuation boundary, Tab or
|
|
111
|
+
Alt+Enter queues a sequential follow-up turn, and pending input stays visible
|
|
112
|
+
and can be withdrawn with Up. The TUI also includes a
|
|
110
113
|
shared-command
|
|
111
114
|
slash palette, tabbed help and shortcut surfaces, searchable resume picker,
|
|
112
115
|
restored active-branch conversation history, streaming and expandable
|
|
@@ -9,6 +9,7 @@ import { type ClaudeInterruptionClassification } from '../native/interruption.js
|
|
|
9
9
|
import { type ModelDocument, type ModelImage, type ModelToolCall, type ModelProvider, type ModelUsage, type PermissionApproval, type PermissionDecision, type PermissionResolver, type PermissionUpdate, type RuntimeEventSink, type ToolRegistry } from '../core/runtime.js';
|
|
10
10
|
import { type BackgroundTaskSnapshot } from './background-task-runtime.js';
|
|
11
11
|
import type { ModelPricingRegistry } from '../core/usage.js';
|
|
12
|
+
import { type ActiveTurnInputCommandResult } from '../core/active-turn-input.js';
|
|
12
13
|
import type { Compactor } from '../core/compaction.js';
|
|
13
14
|
import { ContextBudget } from '../core/context-budget.js';
|
|
14
15
|
import { type ContextAssembler } from '../core/context.js';
|
|
@@ -227,6 +228,7 @@ export declare class ClaudeSessionService {
|
|
|
227
228
|
private readonly hookLifecycle;
|
|
228
229
|
private readonly leadOperations;
|
|
229
230
|
private readonly fileChangeWatcher;
|
|
231
|
+
private readonly activeTurnInputs;
|
|
230
232
|
private runtimeCwd;
|
|
231
233
|
constructor(options: ClaudeSessionServiceOptions);
|
|
232
234
|
nextScheduledPrompt(signal?: AbortSignal): Promise<ScheduledPrompt | null>;
|
|
@@ -270,6 +272,8 @@ export declare class ClaudeSessionService {
|
|
|
270
272
|
run(prompt: string, signal?: AbortSignal, sessionId?: string, name?: string, images?: readonly ModelImage[], documents?: readonly ModelDocument[]): Promise<SessionRunResult>;
|
|
271
273
|
runShell(command: string, signal?: AbortSignal, sessionId?: string, name?: string): Promise<SessionRunResult>;
|
|
272
274
|
resume(sessionId: string, prompt: string, signal?: AbortSignal, name?: string, images?: readonly ModelImage[], documents?: readonly ModelDocument[], resumeSessionAt?: string): Promise<SessionRunResult>;
|
|
275
|
+
steer(sessionId: string, content: string): ActiveTurnInputCommandResult;
|
|
276
|
+
withdrawSteering(sessionId: string, id: string): ActiveTurnInputCommandResult;
|
|
273
277
|
resumeShell(sessionId: string, command: string, signal?: AbortSignal, name?: string, resumeSessionAt?: string): Promise<SessionRunResult>;
|
|
274
278
|
answerSideQuestion(sessionId: string | undefined, question: string, signal?: AbortSignal, onDelta?: (delta: string) => void, permissionMode?: ClaudePermissionMode): Promise<SideQuestionResult>;
|
|
275
279
|
forkSideQuestion(sessionId: string, question: string, signal?: AbortSignal): Promise<SideQuestionForkResult>;
|
|
@@ -21,6 +21,7 @@ import { BackgroundTaskRuntime, } from './background-task-runtime.js';
|
|
|
21
21
|
import { backgroundAgentNotificationMarkers, } from './background-agent-manager.js';
|
|
22
22
|
import { usageCostUsd } from '../core/usage.js';
|
|
23
23
|
import { isSessionId } from '../core/session.js';
|
|
24
|
+
import { ActiveTurnInputMailbox, } from '../core/active-turn-input.js';
|
|
24
25
|
import { ContextBudget, estimateModelRequestTokens, isPromptTooLongError, } from '../core/context-budget.js';
|
|
25
26
|
import { ContextEngine } from './context-engine.js';
|
|
26
27
|
import { TurnMemoryCoordinator } from './turn-memory-coordinator.js';
|
|
@@ -682,6 +683,7 @@ export class ClaudeSessionService {
|
|
|
682
683
|
hookLifecycle;
|
|
683
684
|
leadOperations;
|
|
684
685
|
fileChangeWatcher;
|
|
686
|
+
activeTurnInputs = new Map();
|
|
685
687
|
runtimeCwd;
|
|
686
688
|
constructor(options) {
|
|
687
689
|
const dataPlane = options.dataPlane ?? 'native';
|
|
@@ -887,6 +889,18 @@ export class ClaudeSessionService {
|
|
|
887
889
|
}
|
|
888
890
|
async close() {
|
|
889
891
|
this.closing = true;
|
|
892
|
+
for (const { mailbox } of this.activeTurnInputs.values()) {
|
|
893
|
+
if (!mailbox)
|
|
894
|
+
continue;
|
|
895
|
+
for (const item of mailbox.close()) {
|
|
896
|
+
this.options.eventSink?.({
|
|
897
|
+
type: 'user-input-rejected',
|
|
898
|
+
id: item.id,
|
|
899
|
+
content: item.content,
|
|
900
|
+
reason: 'closed',
|
|
901
|
+
});
|
|
902
|
+
}
|
|
903
|
+
}
|
|
890
904
|
await this.fileChangeWatcher?.close(5_000);
|
|
891
905
|
await this.hookLifecycle.close();
|
|
892
906
|
await this.drainDetachedHookRuns(5_000);
|
|
@@ -1299,6 +1313,30 @@ export class ClaudeSessionService {
|
|
|
1299
1313
|
...(signal ? { signal } : {}),
|
|
1300
1314
|
});
|
|
1301
1315
|
}
|
|
1316
|
+
steer(sessionId, content) {
|
|
1317
|
+
const active = this.activeTurnInputs.get(sessionId);
|
|
1318
|
+
if (!active)
|
|
1319
|
+
return { kind: 'no-active-turn' };
|
|
1320
|
+
const mailbox = active.mailbox;
|
|
1321
|
+
if (!mailbox)
|
|
1322
|
+
return { kind: 'not-steerable' };
|
|
1323
|
+
const result = mailbox.enqueue(content);
|
|
1324
|
+
if (result.kind === 'accepted')
|
|
1325
|
+
return result;
|
|
1326
|
+
if (result.kind === 'empty')
|
|
1327
|
+
return result;
|
|
1328
|
+
return { kind: 'turn-completing' };
|
|
1329
|
+
}
|
|
1330
|
+
withdrawSteering(sessionId, id) {
|
|
1331
|
+
const active = this.activeTurnInputs.get(sessionId);
|
|
1332
|
+
if (!active)
|
|
1333
|
+
return { kind: 'no-active-turn' };
|
|
1334
|
+
const mailbox = active.mailbox;
|
|
1335
|
+
if (!mailbox)
|
|
1336
|
+
return { kind: 'not-steerable' };
|
|
1337
|
+
const result = mailbox.withdraw(id);
|
|
1338
|
+
return result.kind === 'withdrawn' ? result : { kind: 'not-pending' };
|
|
1339
|
+
}
|
|
1302
1340
|
async resumeShell(sessionId, command, signal, name, resumeSessionAt) {
|
|
1303
1341
|
this.worktreeManager?.bindSession(sessionId);
|
|
1304
1342
|
return this.executeTurn({
|
|
@@ -2449,6 +2487,8 @@ export class ClaudeSessionService {
|
|
|
2449
2487
|
const shellCommand = submission.kind === 'shell' ? submission.command : undefined;
|
|
2450
2488
|
const skipUserPrompt = submission.kind === 'retry';
|
|
2451
2489
|
const controller = new TurnTerminalController(this.options.eventSink ?? (() => undefined));
|
|
2490
|
+
let activeTurnInput;
|
|
2491
|
+
let activeTurnRecord;
|
|
2452
2492
|
try {
|
|
2453
2493
|
this.assertTurnWritable();
|
|
2454
2494
|
if (prompt.length === 0 && images.length === 0 && documents.length === 0)
|
|
@@ -2459,6 +2499,16 @@ export class ClaudeSessionService {
|
|
|
2459
2499
|
if (shellCommand !== undefined && shellCommand.trim().length === 0) {
|
|
2460
2500
|
throw new Error('Shell command must not be empty');
|
|
2461
2501
|
}
|
|
2502
|
+
if (this.activeTurnInputs.has(sessionId)) {
|
|
2503
|
+
throw new Error(`conflict: locked (session ${sessionId} already has an active turn)`);
|
|
2504
|
+
}
|
|
2505
|
+
activeTurnRecord =
|
|
2506
|
+
shellCommand === undefined
|
|
2507
|
+
? {
|
|
2508
|
+
mailbox: (activeTurnInput = new ActiveTurnInputMailbox(randomUUID)),
|
|
2509
|
+
}
|
|
2510
|
+
: {};
|
|
2511
|
+
this.activeTurnInputs.set(sessionId, activeTurnRecord);
|
|
2462
2512
|
await this.activateSessionCostTracker(sessionId);
|
|
2463
2513
|
await this.ensureFileResources(sessionId, signal);
|
|
2464
2514
|
this.worktreeManager?.bindSession(sessionId);
|
|
@@ -3213,6 +3263,24 @@ export class ClaudeSessionService {
|
|
|
3213
3263
|
}
|
|
3214
3264
|
await durableFollowUps.followUpUserMessagesCompleted(messages);
|
|
3215
3265
|
},
|
|
3266
|
+
userInputDelivered: async (item) => {
|
|
3267
|
+
if (nativeLease) {
|
|
3268
|
+
await nativeLease.appendMessages({
|
|
3269
|
+
messages: [{ role: 'user', content: item.content }],
|
|
3270
|
+
});
|
|
3271
|
+
currentTurnUserMessages?.push(item.content);
|
|
3272
|
+
return;
|
|
3273
|
+
}
|
|
3274
|
+
const [steeringEntry] = translateProviderEvents([{ type: 'user-text-block', text: item.content }], this.translationContext(sessionId, snapshot));
|
|
3275
|
+
if (!steeringEntry)
|
|
3276
|
+
throw new Error('Could not translate steering message');
|
|
3277
|
+
const steeringTail = await this.append(lease, snapshot.tail, steeringEntry);
|
|
3278
|
+
snapshot = {
|
|
3279
|
+
entries: [...snapshot.entries, steeringEntry],
|
|
3280
|
+
tail: steeringTail,
|
|
3281
|
+
};
|
|
3282
|
+
currentTurnUserMessages?.push(item.content);
|
|
3283
|
+
},
|
|
3216
3284
|
};
|
|
3217
3285
|
let turnCompleted = false;
|
|
3218
3286
|
try {
|
|
@@ -3974,6 +4042,7 @@ export class ClaudeSessionService {
|
|
|
3974
4042
|
cwd: this.activeCwd(),
|
|
3975
4043
|
toolResultDirectory,
|
|
3976
4044
|
observer,
|
|
4045
|
+
...(activeTurnInput ? { steering: activeTurnInput } : {}),
|
|
3977
4046
|
...(this.options.effort ? { effort: this.options.effort } : {}),
|
|
3978
4047
|
...(this.options.maxModelTurns !== undefined
|
|
3979
4048
|
? { maxModelTurns: this.options.maxModelTurns }
|
|
@@ -4373,6 +4442,20 @@ export class ClaudeSessionService {
|
|
|
4373
4442
|
controller.fail(error, signal);
|
|
4374
4443
|
throw error;
|
|
4375
4444
|
}
|
|
4445
|
+
finally {
|
|
4446
|
+
if (activeTurnInput !== undefined) {
|
|
4447
|
+
for (const item of activeTurnInput.close()) {
|
|
4448
|
+
this.options.eventSink?.({
|
|
4449
|
+
type: 'user-input-rejected',
|
|
4450
|
+
id: item.id,
|
|
4451
|
+
content: item.content,
|
|
4452
|
+
reason: signal?.aborted ? 'cancelled' : 'failed',
|
|
4453
|
+
});
|
|
4454
|
+
}
|
|
4455
|
+
}
|
|
4456
|
+
if (this.activeTurnInputs.get(sessionId) === activeTurnRecord)
|
|
4457
|
+
this.activeTurnInputs.delete(sessionId);
|
|
4458
|
+
}
|
|
4376
4459
|
}
|
|
4377
4460
|
async ensureFileResources(sessionId, signal) {
|
|
4378
4461
|
const resources = this.options.fileResources ?? [];
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { ForkResult, ManualCompactResult, ManualCompactSelection, RewindPoint, SessionForkCheckpoint, SessionRunResult, SessionSummary, SideQuestionForkResult, SideQuestionResult } from '../application/session-service.js';
|
|
2
2
|
import type { ClaudeSessionCostSnapshot } from '../application/session-cost-tracker.js';
|
|
3
3
|
import type { ModelImage, ModelToolCall, PermissionApproval, PermissionDecision, RuntimeEventSink } from '../core/runtime.js';
|
|
4
|
+
import type { ActiveTurnInputCommandResult } from '../core/active-turn-input.js';
|
|
4
5
|
import { type DataPlane } from '../persistence/data-plane.js';
|
|
5
6
|
import type { CliElicitationRequest, CliElicitationResult, CliRuntimeInfo } from './protocol.js';
|
|
6
7
|
import type { ClaudeInteractiveToolCallbacks } from '../tools/claude-interactive-tools.js';
|
|
@@ -34,6 +35,8 @@ import type { BackgroundTaskSnapshot } from '../application/background-task-runt
|
|
|
34
35
|
interface InteractiveSessionCommands {
|
|
35
36
|
run(prompt: string, signal?: AbortSignal, sessionId?: string, name?: string, images?: readonly ModelImage[]): Promise<SessionRunResult>;
|
|
36
37
|
resume(sessionId: string, prompt: string, signal?: AbortSignal, name?: string, images?: readonly ModelImage[]): Promise<SessionRunResult>;
|
|
38
|
+
steer?(sessionId: string, content: string): ActiveTurnInputCommandResult;
|
|
39
|
+
withdrawSteering?(sessionId: string, id: string): ActiveTurnInputCommandResult;
|
|
37
40
|
runShell?(command: string, signal?: AbortSignal, sessionId?: string, name?: string): Promise<SessionRunResult>;
|
|
38
41
|
resumeShell?(sessionId: string, command: string, signal?: AbortSignal): Promise<SessionRunResult>;
|
|
39
42
|
fork(sessionId: string, targetSessionId?: string, resumeSessionAt?: string): Promise<ForkResult>;
|
package/dist/cli/interactive.js
CHANGED
|
@@ -375,6 +375,15 @@ export function InteractiveApp({ dataPlane = resolveDataPlane(), configRoot: sup
|
|
|
375
375
|
const inputHistoryRef = useRef([]);
|
|
376
376
|
const inputHistoryIndexRef = useRef(null);
|
|
377
377
|
const inputHistoryDraftRef = useRef('');
|
|
378
|
+
const [pendingInputs, setPendingInputs] = useState([]);
|
|
379
|
+
const pendingInputsRef = useRef(pendingInputs);
|
|
380
|
+
pendingInputsRef.current = pendingInputs;
|
|
381
|
+
const followUpQueueRef = useRef([]);
|
|
382
|
+
const updatePendingInputs = (update) => {
|
|
383
|
+
const next = typeof update === 'function' ? update(pendingInputsRef.current) : update;
|
|
384
|
+
pendingInputsRef.current = next;
|
|
385
|
+
setPendingInputs(next);
|
|
386
|
+
};
|
|
378
387
|
const undoStackRef = useRef([]);
|
|
379
388
|
const composerImagesRef = useRef(new Map());
|
|
380
389
|
const nextImageIdRef = useRef(1);
|
|
@@ -1728,6 +1737,30 @@ export function InteractiveApp({ dataPlane = resolveDataPlane(), configRoot: sup
|
|
|
1728
1737
|
inputHistoryIndexRef.current = nextIndex;
|
|
1729
1738
|
updateComposerInput(history[nextIndex] ?? '');
|
|
1730
1739
|
};
|
|
1740
|
+
const withdrawLatestPending = () => {
|
|
1741
|
+
if (inputRef.current.trim().length !== 0)
|
|
1742
|
+
return false;
|
|
1743
|
+
const pending = pendingInputsRef.current.at(-1);
|
|
1744
|
+
if (!pending)
|
|
1745
|
+
return false;
|
|
1746
|
+
if (pending.kind === 'follow-up') {
|
|
1747
|
+
followUpQueueRef.current = followUpQueueRef.current.filter((item) => item.id !== pending.id);
|
|
1748
|
+
updatePendingInputs((current) => current.filter((item) => item.id !== pending.id));
|
|
1749
|
+
updateComposerInput(pending.text);
|
|
1750
|
+
return true;
|
|
1751
|
+
}
|
|
1752
|
+
const activeSessionId = sessionIdRef.current;
|
|
1753
|
+
const result = activeSessionId
|
|
1754
|
+
? serviceRef.current?.withdrawSteering?.(activeSessionId, pending.id)
|
|
1755
|
+
: undefined;
|
|
1756
|
+
if (result?.kind === 'withdrawn') {
|
|
1757
|
+
updatePendingInputs((current) => current.filter((item) => item.id !== pending.id));
|
|
1758
|
+
updateComposerInput(result.item.content);
|
|
1759
|
+
return true;
|
|
1760
|
+
}
|
|
1761
|
+
append({ kind: 'warning', text: 'Input is already delivered.' });
|
|
1762
|
+
return true;
|
|
1763
|
+
};
|
|
1731
1764
|
const dismissExitConfirmation = () => {
|
|
1732
1765
|
if (!exitConfirmation)
|
|
1733
1766
|
return;
|
|
@@ -1781,6 +1814,33 @@ export function InteractiveApp({ dataPlane = resolveDataPlane(), configRoot: sup
|
|
|
1781
1814
|
case 'user-message':
|
|
1782
1815
|
append({ kind: 'assistant', text: event.message });
|
|
1783
1816
|
break;
|
|
1817
|
+
case 'user-input-delivered':
|
|
1818
|
+
// Steering is delivered after the preceding assistant batch has been
|
|
1819
|
+
// persisted. Commit that batch to the visible transcript before the
|
|
1820
|
+
// user row, then start the next provider continuation with a fresh
|
|
1821
|
+
// active buffer. Otherwise the active row would appear after the
|
|
1822
|
+
// steering message and be discarded when the turn finally completes.
|
|
1823
|
+
if ((streamingFrameRef.current?.text.trim().length ?? 0) > 0) {
|
|
1824
|
+
append({
|
|
1825
|
+
kind: 'assistant',
|
|
1826
|
+
text: streamingFrameRef.current?.text ?? '',
|
|
1827
|
+
});
|
|
1828
|
+
}
|
|
1829
|
+
streamingFrameRef.current?.resetText();
|
|
1830
|
+
streamingFrameRef.current?.resetThinking();
|
|
1831
|
+
streamingFrameRef.current?.flush();
|
|
1832
|
+
updatePendingInputs((current) => current.filter((item) => item.id !== event.id));
|
|
1833
|
+
append({ kind: 'user', text: event.content });
|
|
1834
|
+
break;
|
|
1835
|
+
case 'user-input-rejected':
|
|
1836
|
+
updatePendingInputs((current) => current.filter((item) => item.id !== event.id));
|
|
1837
|
+
if (inputRef.current.trim().length === 0)
|
|
1838
|
+
updateComposerInput(event.content);
|
|
1839
|
+
append({
|
|
1840
|
+
kind: 'warning',
|
|
1841
|
+
text: `Input not delivered · ${redactSensitiveText(event.content, sensitiveValues)}`,
|
|
1842
|
+
});
|
|
1843
|
+
break;
|
|
1784
1844
|
case 'state':
|
|
1785
1845
|
if (event.state === 'awaiting-model') {
|
|
1786
1846
|
activeAttemptThinkingItemsRef.current = [];
|
|
@@ -3212,7 +3272,7 @@ export function InteractiveApp({ dataPlane = resolveDataPlane(), configRoot: sup
|
|
|
3212
3272
|
onTurnChange?.(restoring);
|
|
3213
3273
|
void restoring.finally(() => onTurnChange?.(null));
|
|
3214
3274
|
};
|
|
3215
|
-
const
|
|
3275
|
+
const submitTurn = async (prompt, shellCommand, images = [], followUpId, internal = false) => {
|
|
3216
3276
|
setTranscriptScrollOffset(0);
|
|
3217
3277
|
const turnNumber = turnNumberRef.current + 1;
|
|
3218
3278
|
const turnStartedAt = Date.now();
|
|
@@ -3227,6 +3287,9 @@ export function InteractiveApp({ dataPlane = resolveDataPlane(), configRoot: sup
|
|
|
3227
3287
|
? AbortSignal.any([signal, turnController.signal])
|
|
3228
3288
|
: turnController.signal;
|
|
3229
3289
|
setBusy(true);
|
|
3290
|
+
if (followUpId !== undefined) {
|
|
3291
|
+
updatePendingInputs((current) => current.filter((item) => item.id !== followUpId));
|
|
3292
|
+
}
|
|
3230
3293
|
setTurnDuration(undefined);
|
|
3231
3294
|
setCommandPaletteOpen(false);
|
|
3232
3295
|
const submittedCommandName = /^\/([^\s]+)/u.exec(prompt)?.[1]?.toLowerCase();
|
|
@@ -3245,9 +3308,10 @@ export function InteractiveApp({ dataPlane = resolveDataPlane(), configRoot: sup
|
|
|
3245
3308
|
else
|
|
3246
3309
|
turnMutatedFilesRef.current = true;
|
|
3247
3310
|
let commands;
|
|
3311
|
+
let turnSucceeded = false;
|
|
3248
3312
|
try {
|
|
3249
3313
|
commands = await service();
|
|
3250
|
-
let activeSessionId =
|
|
3314
|
+
let activeSessionId = sessionIdRef.current;
|
|
3251
3315
|
const startedNewSession = activeSessionId === null;
|
|
3252
3316
|
if (activeSessionId === null) {
|
|
3253
3317
|
activeSessionId = randomUUID();
|
|
@@ -3316,7 +3380,9 @@ export function InteractiveApp({ dataPlane = resolveDataPlane(), configRoot: sup
|
|
|
3316
3380
|
streamingFrameRef.current?.flush();
|
|
3317
3381
|
setStatus('ready');
|
|
3318
3382
|
setTurnDuration(Date.now() - turnStartedAt);
|
|
3319
|
-
if (
|
|
3383
|
+
if (!internal &&
|
|
3384
|
+
followUpQueueRef.current.length === 0 &&
|
|
3385
|
+
runtimeSettingsRef.current.notifChannel !== 'notifications_disabled') {
|
|
3320
3386
|
notifyTerminal({
|
|
3321
3387
|
channel: runtimeSettingsRef.current.notifChannel,
|
|
3322
3388
|
title: 'Praxis',
|
|
@@ -3326,6 +3392,7 @@ export function InteractiveApp({ dataPlane = resolveDataPlane(), configRoot: sup
|
|
|
3326
3392
|
: { write: notificationWriter }),
|
|
3327
3393
|
});
|
|
3328
3394
|
}
|
|
3395
|
+
turnSucceeded = true;
|
|
3329
3396
|
}
|
|
3330
3397
|
catch (error) {
|
|
3331
3398
|
if (turnController.signal.aborted && !signal?.aborted) {
|
|
@@ -3343,7 +3410,7 @@ export function InteractiveApp({ dataPlane = resolveDataPlane(), configRoot: sup
|
|
|
3343
3410
|
}
|
|
3344
3411
|
}
|
|
3345
3412
|
finally {
|
|
3346
|
-
if (!factory.scheduledPrompts && commands) {
|
|
3413
|
+
if (!internal && !factory.scheduledPrompts && commands) {
|
|
3347
3414
|
try {
|
|
3348
3415
|
await commands.close?.();
|
|
3349
3416
|
}
|
|
@@ -3360,6 +3427,62 @@ export function InteractiveApp({ dataPlane = resolveDataPlane(), configRoot: sup
|
|
|
3360
3427
|
}
|
|
3361
3428
|
if (turnControllerRef.current === turnController)
|
|
3362
3429
|
turnControllerRef.current = null;
|
|
3430
|
+
if (!internal) {
|
|
3431
|
+
setBusy(false);
|
|
3432
|
+
}
|
|
3433
|
+
}
|
|
3434
|
+
return turnSucceeded;
|
|
3435
|
+
};
|
|
3436
|
+
const submit = async (prompt, shellCommand, images = [], followUpId) => {
|
|
3437
|
+
let currentPrompt = prompt;
|
|
3438
|
+
let currentShellCommand = shellCommand;
|
|
3439
|
+
let currentImages = images;
|
|
3440
|
+
let currentFollowUpId = followUpId;
|
|
3441
|
+
let completed = false;
|
|
3442
|
+
try {
|
|
3443
|
+
while (true) {
|
|
3444
|
+
completed = await submitTurn(currentPrompt, currentShellCommand, currentImages, currentFollowUpId, true);
|
|
3445
|
+
if (!completed)
|
|
3446
|
+
break;
|
|
3447
|
+
const next = followUpQueueRef.current.shift();
|
|
3448
|
+
if (!next)
|
|
3449
|
+
break;
|
|
3450
|
+
currentPrompt = next.text;
|
|
3451
|
+
currentShellCommand = undefined;
|
|
3452
|
+
currentImages = [];
|
|
3453
|
+
currentFollowUpId = next.id;
|
|
3454
|
+
}
|
|
3455
|
+
if (completed &&
|
|
3456
|
+
runtimeSettingsRef.current.notifChannel !== 'notifications_disabled') {
|
|
3457
|
+
notifyTerminal({
|
|
3458
|
+
channel: runtimeSettingsRef.current.notifChannel,
|
|
3459
|
+
title: 'Praxis',
|
|
3460
|
+
message: 'Turn complete',
|
|
3461
|
+
...(notificationWriter === undefined
|
|
3462
|
+
? {}
|
|
3463
|
+
: { write: notificationWriter }),
|
|
3464
|
+
});
|
|
3465
|
+
}
|
|
3466
|
+
}
|
|
3467
|
+
finally {
|
|
3468
|
+
if (!factory.scheduledPrompts) {
|
|
3469
|
+
const commands = serviceRef.current;
|
|
3470
|
+
if (commands) {
|
|
3471
|
+
try {
|
|
3472
|
+
await commands.close?.();
|
|
3473
|
+
}
|
|
3474
|
+
catch (error) {
|
|
3475
|
+
append({
|
|
3476
|
+
kind: 'warning',
|
|
3477
|
+
text: redactSensitiveText(error instanceof Error ? error.message : String(error), sensitiveValues),
|
|
3478
|
+
});
|
|
3479
|
+
}
|
|
3480
|
+
finally {
|
|
3481
|
+
if (serviceRef.current === commands)
|
|
3482
|
+
serviceRef.current = null;
|
|
3483
|
+
}
|
|
3484
|
+
}
|
|
3485
|
+
}
|
|
3363
3486
|
setBusy(false);
|
|
3364
3487
|
}
|
|
3365
3488
|
};
|
|
@@ -5890,6 +6013,94 @@ export function InteractiveApp({ dataPlane = resolveDataPlane(), configRoot: sup
|
|
|
5890
6013
|
void backgrounding;
|
|
5891
6014
|
return;
|
|
5892
6015
|
}
|
|
6016
|
+
const pendingText = inputRef.current.trim();
|
|
6017
|
+
const addPending = (item) => updatePendingInputs((current) => [...current, item]);
|
|
6018
|
+
if (key.upArrow && pendingText.length === 0) {
|
|
6019
|
+
withdrawLatestPending();
|
|
6020
|
+
return;
|
|
6021
|
+
}
|
|
6022
|
+
if (key.return && key.shift) {
|
|
6023
|
+
updateComposerEditor(insertComposerText(editor(), '\n'));
|
|
6024
|
+
return;
|
|
6025
|
+
}
|
|
6026
|
+
if (isKeybinding('chat:submit') && !key.shift && !key.meta && !key.ctrl) {
|
|
6027
|
+
if (!pendingText)
|
|
6028
|
+
return;
|
|
6029
|
+
const controller = turnControllerRef.current;
|
|
6030
|
+
if (!controller) {
|
|
6031
|
+
append({
|
|
6032
|
+
kind: 'warning',
|
|
6033
|
+
text: 'Turn is completing; input was retained.',
|
|
6034
|
+
});
|
|
6035
|
+
return;
|
|
6036
|
+
}
|
|
6037
|
+
const restoreSteeringText = () => {
|
|
6038
|
+
const current = inputRef.current;
|
|
6039
|
+
updateComposerInput(current.trim().length === 0
|
|
6040
|
+
? pendingText
|
|
6041
|
+
: `${pendingText}\n${current}`);
|
|
6042
|
+
};
|
|
6043
|
+
// Service construction may still be in flight while the busy composer
|
|
6044
|
+
// is already visible. Clear now so newly typed text becomes a distinct
|
|
6045
|
+
// draft; restore both drafts if the active-turn command loses a race.
|
|
6046
|
+
clearComposerInput();
|
|
6047
|
+
const steer = async () => {
|
|
6048
|
+
const commands = serviceRef.current ?? (await service());
|
|
6049
|
+
if (turnControllerRef.current !== controller ||
|
|
6050
|
+
controller.signal.aborted) {
|
|
6051
|
+
restoreSteeringText();
|
|
6052
|
+
append({
|
|
6053
|
+
kind: 'warning',
|
|
6054
|
+
text: 'Active turn changed; input was retained.',
|
|
6055
|
+
});
|
|
6056
|
+
return;
|
|
6057
|
+
}
|
|
6058
|
+
const activeSessionId = sessionIdRef.current;
|
|
6059
|
+
const result = activeSessionId
|
|
6060
|
+
? commands.steer?.(activeSessionId, pendingText)
|
|
6061
|
+
: undefined;
|
|
6062
|
+
if (result?.kind === 'accepted') {
|
|
6063
|
+
addPending({
|
|
6064
|
+
id: result.item.id,
|
|
6065
|
+
kind: 'steering',
|
|
6066
|
+
text: result.item.content,
|
|
6067
|
+
});
|
|
6068
|
+
}
|
|
6069
|
+
else {
|
|
6070
|
+
restoreSteeringText();
|
|
6071
|
+
append({
|
|
6072
|
+
kind: 'warning',
|
|
6073
|
+
text: result?.kind === 'turn-completing'
|
|
6074
|
+
? 'Turn is completing; input was retained.'
|
|
6075
|
+
: result?.kind === 'not-steerable'
|
|
6076
|
+
? 'This turn cannot be steered; input was retained.'
|
|
6077
|
+
: 'Steering is unavailable; input was retained.',
|
|
6078
|
+
});
|
|
6079
|
+
}
|
|
6080
|
+
};
|
|
6081
|
+
void steer().catch((error) => {
|
|
6082
|
+
restoreSteeringText();
|
|
6083
|
+
warn(error);
|
|
6084
|
+
});
|
|
6085
|
+
return;
|
|
6086
|
+
}
|
|
6087
|
+
if (key.tab || (key.return && key.meta)) {
|
|
6088
|
+
if (!pendingText)
|
|
6089
|
+
return;
|
|
6090
|
+
if (!turnControllerRef.current) {
|
|
6091
|
+
append({
|
|
6092
|
+
kind: 'warning',
|
|
6093
|
+
text: 'Turn is completing; follow-up input was retained.',
|
|
6094
|
+
});
|
|
6095
|
+
return;
|
|
6096
|
+
}
|
|
6097
|
+
const item = { id: randomUUID(), text: pendingText };
|
|
6098
|
+
followUpQueueRef.current.push(item);
|
|
6099
|
+
addPending({ id: item.id, kind: 'follow-up', text: item.text });
|
|
6100
|
+
clearComposerInput();
|
|
6101
|
+
return;
|
|
6102
|
+
}
|
|
6103
|
+
editComposer();
|
|
5893
6104
|
return;
|
|
5894
6105
|
}
|
|
5895
6106
|
if (isKeybinding('chat:imagePaste')) {
|
|
@@ -6571,6 +6782,11 @@ export function InteractiveApp({ dataPlane = resolveDataPlane(), configRoot: sup
|
|
|
6571
6782
|
}
|
|
6572
6783
|
return;
|
|
6573
6784
|
}
|
|
6785
|
+
if (isKeybinding('history:previous') &&
|
|
6786
|
+
inputRef.current.length === 0 &&
|
|
6787
|
+
withdrawLatestPending()) {
|
|
6788
|
+
return;
|
|
6789
|
+
}
|
|
6574
6790
|
if (isKeybinding('history:previous')) {
|
|
6575
6791
|
restorePromptHistory('previous');
|
|
6576
6792
|
return;
|
|
@@ -6664,6 +6880,7 @@ export function InteractiveApp({ dataPlane = resolveDataPlane(), configRoot: sup
|
|
|
6664
6880
|
shellMode,
|
|
6665
6881
|
busy,
|
|
6666
6882
|
status: conciseStatus,
|
|
6883
|
+
pendingItems: pendingInputs,
|
|
6667
6884
|
display: runtimeDisplay,
|
|
6668
6885
|
}), [
|
|
6669
6886
|
screen,
|
|
@@ -6672,6 +6889,7 @@ export function InteractiveApp({ dataPlane = resolveDataPlane(), configRoot: sup
|
|
|
6672
6889
|
inputCursor,
|
|
6673
6890
|
busy,
|
|
6674
6891
|
conciseStatus,
|
|
6892
|
+
pendingInputs,
|
|
6675
6893
|
runtimeDisplay,
|
|
6676
6894
|
]);
|
|
6677
6895
|
const terminalSelectionContext = useMemo(() => createTerminalSelectionContext(quietFrame, conversationScreen?.transcript.rows.length ?? 0, transcriptScrollOffset, maxTranscriptScrollOffset), [
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { TuiScreenModel } from './tui-screen-model.js';
|
|
2
2
|
import type { TuiRow, TuiTextRole } from './tui-row-ir.js';
|
|
3
3
|
export type QuietFrameDensity = 'full' | 'standard' | 'compact' | 'narrow' | 'minimal';
|
|
4
|
-
export type QuietFrameRegion = 'identity' | 'transcript' | 'active' | 'focus' | 'composer' | 'status';
|
|
4
|
+
export type QuietFrameRegion = 'identity' | 'transcript' | 'active' | 'pending' | 'focus' | 'composer' | 'status';
|
|
5
5
|
export interface QuietFrameRow extends TuiRow {
|
|
6
6
|
readonly region: QuietFrameRegion;
|
|
7
7
|
readonly accessibleText?: string;
|
|
@@ -26,6 +26,11 @@ export interface QuietFrameInput {
|
|
|
26
26
|
readonly shellMode: boolean;
|
|
27
27
|
readonly busy: boolean;
|
|
28
28
|
readonly status: string;
|
|
29
|
+
readonly pendingItems?: readonly {
|
|
30
|
+
readonly id: string;
|
|
31
|
+
readonly kind: 'steering' | 'follow-up';
|
|
32
|
+
readonly text: string;
|
|
33
|
+
}[];
|
|
29
34
|
readonly display?: {
|
|
30
35
|
readonly cwd?: string;
|
|
31
36
|
readonly model?: string;
|
|
@@ -280,6 +280,10 @@ export function projectQuietFrame(input) {
|
|
|
280
280
|
thinking.lines.forEach(({ text: part, start }, index) => lines.push(createQuietFrameRow(`quiet:active:thinking:${start}`, `${index === 0 ? '✻ ' : ' '}${part}`, 'active', 'muted', `${index === 0 ? 'Thinking: ' : ''}${part}`)));
|
|
281
281
|
}
|
|
282
282
|
}
|
|
283
|
+
for (const item of input.pendingItems ?? []) {
|
|
284
|
+
const label = item.kind === 'steering' ? 'steer' : 'follow-up';
|
|
285
|
+
lines.push(createQuietFrameRow(`quiet:pending:${item.id}`, `${label} · ${clean(item.text)}`, 'pending', 'muted', `${label} pending input: ${clean(item.text)}`));
|
|
286
|
+
}
|
|
283
287
|
const focus = input.focusRows.length > 0
|
|
284
288
|
? input.focusRows.map((item) => withRegion(item, 'focus'))
|
|
285
289
|
: [
|
|
@@ -19,6 +19,7 @@ export interface QuietScreenProjectionInput {
|
|
|
19
19
|
readonly shellMode: boolean;
|
|
20
20
|
readonly busy: boolean;
|
|
21
21
|
readonly status: string;
|
|
22
|
+
readonly pendingItems?: QuietFrameInput['pendingItems'];
|
|
22
23
|
readonly display?: QuietFrameInput['display'];
|
|
23
24
|
}
|
|
24
25
|
export declare function projectQuietScreenFrame(input: QuietScreenProjectionInput): QuietFrame;
|
|
@@ -103,6 +103,9 @@ export function projectQuietScreenFrame(input) {
|
|
|
103
103
|
shellMode: input.shellMode,
|
|
104
104
|
busy: input.busy,
|
|
105
105
|
status: input.status,
|
|
106
|
+
...(input.pendingItems === undefined
|
|
107
|
+
? {}
|
|
108
|
+
: { pendingItems: input.pendingItems }),
|
|
106
109
|
...(input.display === undefined ? {} : { display: input.display }),
|
|
107
110
|
focusRows,
|
|
108
111
|
});
|
|
@@ -71,7 +71,7 @@ export function routeTuiInteraction(snapshot, input) {
|
|
|
71
71
|
}
|
|
72
72
|
if (input.action === 'chat:cancel')
|
|
73
73
|
return handled([...confirmationEffects, { kind: 'interrupt-turn' }]);
|
|
74
|
-
return
|
|
74
|
+
return delegated(confirmationEffects);
|
|
75
75
|
}
|
|
76
76
|
if (input.callerIntent !== 'none')
|
|
77
77
|
return delegated(confirmationEffects);
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/** A user input item accepted by an active, steerable turn. */
|
|
2
|
+
export interface SteeringItem {
|
|
3
|
+
readonly id: string;
|
|
4
|
+
readonly content: string;
|
|
5
|
+
}
|
|
6
|
+
export type SteeringEnqueueResult = {
|
|
7
|
+
readonly kind: 'accepted';
|
|
8
|
+
readonly item: SteeringItem;
|
|
9
|
+
} | {
|
|
10
|
+
readonly kind: 'empty';
|
|
11
|
+
} | {
|
|
12
|
+
readonly kind: 'sealed';
|
|
13
|
+
};
|
|
14
|
+
export type SteeringWithdrawResult = {
|
|
15
|
+
readonly kind: 'withdrawn';
|
|
16
|
+
readonly item: SteeringItem;
|
|
17
|
+
} | {
|
|
18
|
+
readonly kind: 'not-pending';
|
|
19
|
+
};
|
|
20
|
+
/** Results returned by the session-level steering commands. */
|
|
21
|
+
export type ActiveTurnInputCommandResult = {
|
|
22
|
+
readonly kind: 'accepted';
|
|
23
|
+
readonly item: SteeringItem;
|
|
24
|
+
} | {
|
|
25
|
+
readonly kind: 'withdrawn';
|
|
26
|
+
readonly item: SteeringItem;
|
|
27
|
+
} | {
|
|
28
|
+
readonly kind: 'empty';
|
|
29
|
+
} | {
|
|
30
|
+
readonly kind: 'no-active-turn';
|
|
31
|
+
} | {
|
|
32
|
+
readonly kind: 'not-steerable';
|
|
33
|
+
} | {
|
|
34
|
+
readonly kind: 'turn-completing';
|
|
35
|
+
} | {
|
|
36
|
+
readonly kind: 'not-pending';
|
|
37
|
+
};
|
|
38
|
+
/** The synchronous port exposed to the runtime at safe continuation points. */
|
|
39
|
+
export interface ActiveTurnInputPort {
|
|
40
|
+
take(): SteeringItem | undefined;
|
|
41
|
+
takeOrSeal(): SteeringItem | undefined;
|
|
42
|
+
/** Drain and seal the port, returning every item that was not delivered. */
|
|
43
|
+
close(): readonly SteeringItem[];
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* A small synchronous FIFO. Keeping all state transitions synchronous makes
|
|
47
|
+
* enqueue/take-or-seal atomic with respect to the runtime's await boundaries.
|
|
48
|
+
*/
|
|
49
|
+
export declare class ActiveTurnInputMailbox implements ActiveTurnInputPort {
|
|
50
|
+
private readonly createId;
|
|
51
|
+
private readonly items;
|
|
52
|
+
private sealed;
|
|
53
|
+
constructor(createId: () => string);
|
|
54
|
+
enqueue(content: string): SteeringEnqueueResult;
|
|
55
|
+
take(): SteeringItem | undefined;
|
|
56
|
+
/** Take one item, or seal an empty mailbox before returning. */
|
|
57
|
+
takeOrSeal(): SteeringItem | undefined;
|
|
58
|
+
withdraw(id: string): SteeringWithdrawResult;
|
|
59
|
+
close(): readonly SteeringItem[];
|
|
60
|
+
isSealed(): boolean;
|
|
61
|
+
get pendingCount(): number;
|
|
62
|
+
}
|
|
63
|
+
//# sourceMappingURL=active-turn-input.d.ts.map
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A small synchronous FIFO. Keeping all state transitions synchronous makes
|
|
3
|
+
* enqueue/take-or-seal atomic with respect to the runtime's await boundaries.
|
|
4
|
+
*/
|
|
5
|
+
export class ActiveTurnInputMailbox {
|
|
6
|
+
createId;
|
|
7
|
+
items = [];
|
|
8
|
+
sealed = false;
|
|
9
|
+
constructor(createId) {
|
|
10
|
+
this.createId = createId;
|
|
11
|
+
}
|
|
12
|
+
enqueue(content) {
|
|
13
|
+
const trimmed = content.trim();
|
|
14
|
+
if (trimmed.length === 0)
|
|
15
|
+
return { kind: 'empty' };
|
|
16
|
+
if (this.sealed)
|
|
17
|
+
return { kind: 'sealed' };
|
|
18
|
+
const item = { id: this.createId(), content: trimmed };
|
|
19
|
+
this.items.push(item);
|
|
20
|
+
return { kind: 'accepted', item };
|
|
21
|
+
}
|
|
22
|
+
take() {
|
|
23
|
+
return this.items.shift();
|
|
24
|
+
}
|
|
25
|
+
/** Take one item, or seal an empty mailbox before returning. */
|
|
26
|
+
takeOrSeal() {
|
|
27
|
+
const item = this.items.shift();
|
|
28
|
+
if (item !== undefined)
|
|
29
|
+
return item;
|
|
30
|
+
this.sealed = true;
|
|
31
|
+
return undefined;
|
|
32
|
+
}
|
|
33
|
+
withdraw(id) {
|
|
34
|
+
const index = this.items.findIndex((item) => item.id === id);
|
|
35
|
+
if (index < 0)
|
|
36
|
+
return { kind: 'not-pending' };
|
|
37
|
+
const [item] = this.items.splice(index, 1);
|
|
38
|
+
return item === undefined
|
|
39
|
+
? { kind: 'not-pending' }
|
|
40
|
+
: { kind: 'withdrawn', item };
|
|
41
|
+
}
|
|
42
|
+
close() {
|
|
43
|
+
this.sealed = true;
|
|
44
|
+
return this.items.splice(0);
|
|
45
|
+
}
|
|
46
|
+
isSealed() {
|
|
47
|
+
return this.sealed;
|
|
48
|
+
}
|
|
49
|
+
get pendingCount() {
|
|
50
|
+
return this.items.length;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
//# sourceMappingURL=active-turn-input.js.map
|
package/dist/core/runtime.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { ActiveTurnInputPort, SteeringItem } from './active-turn-input.js';
|
|
1
2
|
export type RuntimeState = 'idle' | 'assembling-context' | 'compacting' | 'awaiting-model' | 'streaming' | 'awaiting-permission' | 'executing-tools' | 'persisting-results' | 'completed' | 'cancelled' | 'failed';
|
|
2
3
|
export type ModelMessage = {
|
|
3
4
|
role: 'system';
|
|
@@ -175,6 +176,15 @@ export type RuntimeEvent = {
|
|
|
175
176
|
message: string;
|
|
176
177
|
attachments?: readonly string[];
|
|
177
178
|
status: 'normal' | 'proactive';
|
|
179
|
+
} | {
|
|
180
|
+
type: 'user-input-delivered';
|
|
181
|
+
id: string;
|
|
182
|
+
content: string;
|
|
183
|
+
} | {
|
|
184
|
+
type: 'user-input-rejected';
|
|
185
|
+
id: string;
|
|
186
|
+
content: string;
|
|
187
|
+
reason: 'cancelled' | 'failed' | 'closed';
|
|
178
188
|
} | {
|
|
179
189
|
type: 'usage';
|
|
180
190
|
usage: ModelUsage;
|
|
@@ -434,6 +444,7 @@ export interface AgentRunObserver {
|
|
|
434
444
|
}): Promise<void>;
|
|
435
445
|
toolCompleted(call: ModelToolCall, result: ToolExecutionResult): Promise<void>;
|
|
436
446
|
followUpUserMessagesCompleted?(messages: readonly string[]): Promise<void>;
|
|
447
|
+
userInputDelivered?(item: SteeringItem): Promise<void>;
|
|
437
448
|
}
|
|
438
449
|
export interface ToolUseSummaryOutcome {
|
|
439
450
|
summary: string | null;
|
|
@@ -471,6 +482,7 @@ export interface AgentRunRequest {
|
|
|
471
482
|
cwd?: string;
|
|
472
483
|
toolResultDirectory?: string;
|
|
473
484
|
observer?: AgentRunObserver;
|
|
485
|
+
steering?: ActiveTurnInputPort;
|
|
474
486
|
reloadMessages?: () => Promise<readonly ModelMessage[]>;
|
|
475
487
|
approveTool?: (call: ModelToolCall, originalCall?: ModelToolCall, decision?: PermissionDecision) => PermissionApproval | Promise<PermissionApproval>;
|
|
476
488
|
permissionUpdates?: readonly PermissionUpdate[];
|
package/dist/core/runtime.js
CHANGED
|
@@ -255,13 +255,58 @@ export class AgentRuntime {
|
|
|
255
255
|
this.options = options;
|
|
256
256
|
}
|
|
257
257
|
async run(request) {
|
|
258
|
+
const steering = request.steering;
|
|
259
|
+
const rejectSteering = (reason) => {
|
|
260
|
+
for (const item of steering?.close() ?? []) {
|
|
261
|
+
this.emit({
|
|
262
|
+
type: 'user-input-rejected',
|
|
263
|
+
id: item.id,
|
|
264
|
+
content: item.content,
|
|
265
|
+
reason,
|
|
266
|
+
});
|
|
267
|
+
}
|
|
268
|
+
};
|
|
269
|
+
const cancelWithSteering = () => {
|
|
270
|
+
rejectSteering('cancelled');
|
|
271
|
+
return this.cancel();
|
|
272
|
+
};
|
|
273
|
+
const deliverSteering = async (item) => {
|
|
274
|
+
try {
|
|
275
|
+
if (!request.observer?.userInputDelivered) {
|
|
276
|
+
throw new Error('Active-turn steering requires a durable delivery observer');
|
|
277
|
+
}
|
|
278
|
+
await request.observer.userInputDelivered(item);
|
|
279
|
+
}
|
|
280
|
+
catch (error) {
|
|
281
|
+
this.emit({
|
|
282
|
+
type: 'user-input-rejected',
|
|
283
|
+
id: item.id,
|
|
284
|
+
content: item.content,
|
|
285
|
+
reason: 'failed',
|
|
286
|
+
});
|
|
287
|
+
throw error;
|
|
288
|
+
}
|
|
289
|
+
messages.push({ role: 'user', content: item.content });
|
|
290
|
+
this.emit({
|
|
291
|
+
type: 'user-input-delivered',
|
|
292
|
+
id: item.id,
|
|
293
|
+
content: item.content,
|
|
294
|
+
});
|
|
295
|
+
};
|
|
296
|
+
const deliverNextSteering = async () => {
|
|
297
|
+
const item = steering?.take();
|
|
298
|
+
if (!item)
|
|
299
|
+
return false;
|
|
300
|
+
await deliverSteering(item);
|
|
301
|
+
return true;
|
|
302
|
+
};
|
|
258
303
|
const failurePresentationDeferred = (kind) => request.deferFailureKinds === true ||
|
|
259
304
|
request.deferFailureKinds?.includes(kind) === true;
|
|
260
305
|
if (this.options.emitInitialContextState !== false) {
|
|
261
306
|
this.emit({ type: 'state', state: 'assembling-context' });
|
|
262
307
|
}
|
|
263
308
|
if (request.signal?.aborted)
|
|
264
|
-
return
|
|
309
|
+
return cancelWithSteering();
|
|
265
310
|
const messages = [...request.messages];
|
|
266
311
|
let usage = emptyUsage();
|
|
267
312
|
let modelUsage = emptyUsage();
|
|
@@ -295,7 +340,7 @@ export class AgentRuntime {
|
|
|
295
340
|
activeAttemptHasPresentation = false;
|
|
296
341
|
activeAttemptDiscarded = false;
|
|
297
342
|
if (request.signal?.aborted)
|
|
298
|
-
return
|
|
343
|
+
return cancelWithSteering();
|
|
299
344
|
if (maxModelTurns !== undefined && modelTurns >= maxModelTurns) {
|
|
300
345
|
throw new Error(`Maximum model turns of ${maxModelTurns} exceeded`);
|
|
301
346
|
}
|
|
@@ -399,7 +444,7 @@ export class AgentRuntime {
|
|
|
399
444
|
try {
|
|
400
445
|
for await (const event of this.provider.complete(providerRequest)) {
|
|
401
446
|
if (request.signal?.aborted)
|
|
402
|
-
return
|
|
447
|
+
return cancelWithSteering();
|
|
403
448
|
if (terminalReason !== undefined) {
|
|
404
449
|
throw new ModelProviderError(`Provider emitted ${event.type} after terminal reason ${terminalReason}`, { retryable: false });
|
|
405
450
|
}
|
|
@@ -499,7 +544,7 @@ export class AgentRuntime {
|
|
|
499
544
|
}
|
|
500
545
|
await toolScheduler.settle().catch(() => undefined);
|
|
501
546
|
if (request.signal?.aborted)
|
|
502
|
-
return
|
|
547
|
+
return cancelWithSteering();
|
|
503
548
|
throw error;
|
|
504
549
|
}
|
|
505
550
|
finally {
|
|
@@ -582,6 +627,11 @@ export class AgentRuntime {
|
|
|
582
627
|
messages.push(assistantMessage);
|
|
583
628
|
toolScheduler.releaseExclusiveTools();
|
|
584
629
|
if (toolCalls.length === 0) {
|
|
630
|
+
const preStopSteering = steering?.take();
|
|
631
|
+
if (preStopSteering !== undefined) {
|
|
632
|
+
await deliverSteering(preStopSteering);
|
|
633
|
+
continue;
|
|
634
|
+
}
|
|
585
635
|
const stopResult = (await request.onStop?.(text)) ?? [];
|
|
586
636
|
const stopBatch = Array.isArray(stopResult)
|
|
587
637
|
? null
|
|
@@ -617,11 +667,16 @@ export class AgentRuntime {
|
|
|
617
667
|
if (request.reloadMessages) {
|
|
618
668
|
const reloadedMessages = await request.reloadMessages();
|
|
619
669
|
if (request.signal?.aborted)
|
|
620
|
-
return
|
|
670
|
+
return cancelWithSteering();
|
|
621
671
|
messages.splice(0, messages.length, ...reloadedMessages);
|
|
622
672
|
}
|
|
623
673
|
continue;
|
|
624
674
|
}
|
|
675
|
+
const completionInput = steering?.takeOrSeal();
|
|
676
|
+
if (completionInput !== undefined) {
|
|
677
|
+
await deliverSteering(completionInput);
|
|
678
|
+
continue;
|
|
679
|
+
}
|
|
625
680
|
this.emit({ type: 'state', state: 'completed' });
|
|
626
681
|
const modelUsage = modelUsageByModel.size === 0
|
|
627
682
|
? undefined
|
|
@@ -709,14 +764,18 @@ export class AgentRuntime {
|
|
|
709
764
|
if (request.reloadMessages) {
|
|
710
765
|
const reloadedMessages = await request.reloadMessages();
|
|
711
766
|
if (request.signal?.aborted)
|
|
712
|
-
return
|
|
767
|
+
return cancelWithSteering();
|
|
713
768
|
messages.splice(0, messages.length, ...reloadedMessages);
|
|
714
769
|
}
|
|
770
|
+
await deliverNextSteering();
|
|
715
771
|
}
|
|
716
772
|
}
|
|
717
773
|
catch (error) {
|
|
718
|
-
if (request.signal?.aborted)
|
|
774
|
+
if (request.signal?.aborted) {
|
|
775
|
+
rejectSteering('cancelled');
|
|
719
776
|
return this.cancel();
|
|
777
|
+
}
|
|
778
|
+
rejectSteering('failed');
|
|
720
779
|
const message = error instanceof Error ? error.message : String(error);
|
|
721
780
|
const retryable = error instanceof ModelProviderError ? error.retryable : false;
|
|
722
781
|
const kind = error instanceof ModelProviderError
|