@markusylisiurunen/tau 0.3.34 → 0.3.35
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 +6 -1
- package/dist/core/agent/agent_runtime.js +6 -5
- package/dist/core/agent/agent_runtime.js.map +1 -1
- package/dist/core/commands/registry.js +24 -2
- package/dist/core/commands/registry.js.map +1 -1
- package/dist/core/runtime/chat_runtime.js +13 -2
- package/dist/core/runtime/chat_runtime.js.map +1 -1
- package/dist/core/runtime/steering.js +2 -1
- package/dist/core/runtime/steering.js.map +1 -1
- package/dist/core/session/compaction.js +1 -1
- package/dist/core/session/compaction.js.map +1 -1
- package/dist/core/session/goal.js +54 -0
- package/dist/core/session/goal.js.map +1 -0
- package/dist/core/subagents/agent_supervisor.js +4 -1
- package/dist/core/subagents/agent_supervisor.js.map +1 -1
- package/dist/core/tools/catalog.js +14 -1
- package/dist/core/tools/catalog.js.map +1 -1
- package/dist/core/tools/goal.js +77 -0
- package/dist/core/tools/goal.js.map +1 -0
- package/dist/core/tools/tool_names.js +3 -0
- package/dist/core/tools/tool_names.js.map +1 -1
- package/dist/core/utils/user_metadata.js +14 -0
- package/dist/core/utils/user_metadata.js.map +1 -1
- package/dist/core/version.js +1 -1
- package/dist/host/client_tool_broker.js +4 -0
- package/dist/host/client_tool_broker.js.map +1 -1
- package/dist/host/local_session_host.js +391 -45
- package/dist/host/local_session_host.js.map +1 -1
- package/dist/host/session_host.js +2 -0
- package/dist/host/session_host.js.map +1 -1
- package/dist/host/session_protocol_handler.js +253 -236
- package/dist/host/session_protocol_handler.js.map +1 -1
- package/dist/protocol/index.d.ts +1 -1
- package/dist/protocol/index.js.map +1 -1
- package/dist/protocol/session_protocol.d.ts +28 -3
- package/dist/protocol/session_protocol.js +53 -1
- package/dist/protocol/session_protocol.js.map +1 -1
- package/dist/sdk/index.d.ts +1 -1
- package/dist/sdk/index.js.map +1 -1
- package/dist/sdk/session.js +20 -0
- package/dist/sdk/session.js.map +1 -1
- package/dist/sdk/types.d.ts +7 -1
- package/dist/store/session_snapshot_migrations.js +10 -1
- package/dist/store/session_snapshot_migrations.js.map +1 -1
- package/dist/tui/chat_view.js +1 -0
- package/dist/tui/chat_view.js.map +1 -1
- package/dist/tui/session_chat_controller.js +40 -3
- package/dist/tui/session_chat_controller.js.map +1 -1
- package/dist/tui/ui/footer.js +7 -3
- package/dist/tui/ui/footer.js.map +1 -1
- package/package.json +1 -1
|
@@ -2,16 +2,18 @@ import { randomUUID } from "node:crypto";
|
|
|
2
2
|
import { isDeepStrictEqual } from "node:util";
|
|
3
3
|
import { resolvePromptTemplateWithBackend } from "../core/config/index.js";
|
|
4
4
|
import { ChatRuntime } from "../core/runtime/chat_runtime.js";
|
|
5
|
+
import { formatSteeringUserMessage } from "../core/runtime/steering.js";
|
|
6
|
+
import { buildGoalContinuationText, prependGoalPolicy } from "../core/session/goal.js";
|
|
5
7
|
import { SUBAGENT_ACTIVITY_FACET_KIND } from "../core/subagents/types.js";
|
|
6
8
|
import { appendUsageLogEntry, getUsageCostTotal, getUsageTotals, } from "../core/usage/logs.js";
|
|
7
9
|
import { filterProjectPathAutocompleteEntries, loadProjectPathAutocompleteEntriesWithBackend, } from "../core/utils/project_files.js";
|
|
8
|
-
import { hasAutoCompactionContinuationMetadata, isTauUserMessageHidden, } from "../core/utils/user_metadata.js";
|
|
10
|
+
import { hasAutoCompactionContinuationMetadata, hasGoalTurnMetadata, isTauUserMessageHidden, } from "../core/utils/user_metadata.js";
|
|
9
11
|
import { applySessionProtocolDelta, createSessionProtocolDeltaMessage, createSessionProtocolEphemeralMessage, } from "../protocol/session_protocol.js";
|
|
10
12
|
import { LEGACY_SESSION_CONTEXT_EPOCH } from "../store/session_snapshot_migrations.js";
|
|
11
13
|
import { ClientToolBroker } from "./client_tool_broker.js";
|
|
12
14
|
import { createExecutionEnvironmentSubagentRuntimeResolver } from "./execution_runtime.js";
|
|
13
15
|
import { HostedEphemeralAgentSession } from "./hosted_ephemeral_agent_session.js";
|
|
14
|
-
import { SessionExecBusyError, } from "./session_host.js";
|
|
16
|
+
import { SessionExecBusyError, SessionRetryUnavailableError, } from "./session_host.js";
|
|
15
17
|
const PATH_AUTOCOMPLETE_CACHE_TTL_MS = 5_000;
|
|
16
18
|
export class LocalSessionHost {
|
|
17
19
|
sessions = new Set();
|
|
@@ -124,6 +126,11 @@ export class LocalSessionHost {
|
|
|
124
126
|
environment: this.sessionOptions.environment,
|
|
125
127
|
eventSink: async (event) => await hostedSession.enqueueRuntimeEvent(event),
|
|
126
128
|
subagentEventSink: async (event) => await hostedSession.recordSubagentEvent(event),
|
|
129
|
+
goalManager: {
|
|
130
|
+
getGoal: () => hostedSession.getGoal(),
|
|
131
|
+
createGoal: async (objective) => await hostedSession.createGoal(objective),
|
|
132
|
+
updateGoal: async (update) => await hostedSession.updateGoal(update),
|
|
133
|
+
},
|
|
127
134
|
initialPromptComposition: committedSnapshot
|
|
128
135
|
? promptCompositionFromSnapshot(committedSnapshot)
|
|
129
136
|
: undefined,
|
|
@@ -347,9 +354,12 @@ class LocalHostedSessionHandle {
|
|
|
347
354
|
activeWorkPromises = new Set();
|
|
348
355
|
activeExecAbortControllers = new Map();
|
|
349
356
|
activeTurnPromise;
|
|
357
|
+
activeLogicalTurn;
|
|
350
358
|
disposePromise;
|
|
351
359
|
disposing = false;
|
|
352
360
|
costTotal = 0;
|
|
361
|
+
goal;
|
|
362
|
+
pendingGoalCommit;
|
|
353
363
|
forceNextSnapshotRevision;
|
|
354
364
|
disposed = false;
|
|
355
365
|
constructor(runtime, promptBootstrap, catalog, bootstrap, includeAgentContext, executionEnvironment, store, committedSnapshot, forceNextSnapshotRevision, recordUsage, removeFromHost = () => { }) {
|
|
@@ -370,12 +380,17 @@ class LocalHostedSessionHandle {
|
|
|
370
380
|
this.persistedSnapshot = committedSnapshot
|
|
371
381
|
? cloneSessionProtocolSnapshot(committedSnapshot)
|
|
372
382
|
: undefined;
|
|
383
|
+
this.goal = structuredClone(committedSnapshot?.goal ?? null);
|
|
373
384
|
this.forceNextSnapshotRevision = forceNextSnapshotRevision;
|
|
374
385
|
this.restoreProtocolState(committedSnapshot);
|
|
375
386
|
}
|
|
376
387
|
get isTurnRunning() {
|
|
377
388
|
return this.runtime.isTurnRunning;
|
|
378
389
|
}
|
|
390
|
+
get canAcceptSteering() {
|
|
391
|
+
return (this.runtime.isTurnRunning ||
|
|
392
|
+
Boolean(this.activeLogicalTurn && this.goal?.status === "active"));
|
|
393
|
+
}
|
|
379
394
|
get sessionId() {
|
|
380
395
|
return this.session.sessionId;
|
|
381
396
|
}
|
|
@@ -397,6 +412,105 @@ class LocalHostedSessionHandle {
|
|
|
397
412
|
this.ephemeralListeners.delete(handler);
|
|
398
413
|
};
|
|
399
414
|
}
|
|
415
|
+
getGoal() {
|
|
416
|
+
return structuredClone(this.goal);
|
|
417
|
+
}
|
|
418
|
+
async createGoal(objective) {
|
|
419
|
+
const goal = this.buildNewGoal(objective);
|
|
420
|
+
await this.setGoal(goal);
|
|
421
|
+
return structuredClone(goal);
|
|
422
|
+
}
|
|
423
|
+
async updateGoal(update) {
|
|
424
|
+
this.assertActive();
|
|
425
|
+
const current = this.goal;
|
|
426
|
+
if (!current) {
|
|
427
|
+
throw new Error("no goal exists");
|
|
428
|
+
}
|
|
429
|
+
if (update.objective !== undefined && update.status === "complete") {
|
|
430
|
+
throw new Error("goal objective cannot be updated while completing the goal");
|
|
431
|
+
}
|
|
432
|
+
if (update.status === "complete") {
|
|
433
|
+
await this.setGoal(null);
|
|
434
|
+
return null;
|
|
435
|
+
}
|
|
436
|
+
const objective = update.objective?.trim() ?? current.objective;
|
|
437
|
+
if (!objective) {
|
|
438
|
+
throw new Error("goal objective must not be empty");
|
|
439
|
+
}
|
|
440
|
+
const goal = {
|
|
441
|
+
objective,
|
|
442
|
+
status: update.status ?? current.status,
|
|
443
|
+
};
|
|
444
|
+
await this.setGoal(goal);
|
|
445
|
+
return structuredClone(goal);
|
|
446
|
+
}
|
|
447
|
+
async startGoal(objective) {
|
|
448
|
+
const goal = this.buildNewGoal(objective);
|
|
449
|
+
return await this.runLogicalTurn(async (logicalTurn) => {
|
|
450
|
+
this.goal = goal;
|
|
451
|
+
this.pendingGoalCommit = goal;
|
|
452
|
+
try {
|
|
453
|
+
const { userHistoryEntryId } = await this.record({
|
|
454
|
+
text: prependGoalPolicy(goal.objective, goal),
|
|
455
|
+
});
|
|
456
|
+
if (logicalTurn.cancellationRequested) {
|
|
457
|
+
return {
|
|
458
|
+
userHistoryEntryId,
|
|
459
|
+
turn: await this.cancelLogicalTurn(userHistoryEntryId),
|
|
460
|
+
};
|
|
461
|
+
}
|
|
462
|
+
return {
|
|
463
|
+
userHistoryEntryId,
|
|
464
|
+
turn: await this.runTurnNow(logicalTurn, userHistoryEntryId),
|
|
465
|
+
};
|
|
466
|
+
}
|
|
467
|
+
catch (error) {
|
|
468
|
+
if (this.pendingGoalCommit === goal) {
|
|
469
|
+
this.goal = null;
|
|
470
|
+
this.pendingGoalCommit = undefined;
|
|
471
|
+
}
|
|
472
|
+
else {
|
|
473
|
+
await this.blockActiveGoal().catch(() => undefined);
|
|
474
|
+
}
|
|
475
|
+
throw error;
|
|
476
|
+
}
|
|
477
|
+
});
|
|
478
|
+
}
|
|
479
|
+
async resumeGoal() {
|
|
480
|
+
this.assertActive();
|
|
481
|
+
if (this.goal?.status !== "blocked") {
|
|
482
|
+
throw new Error(this.goal ? "goal is already active" : "no goal exists");
|
|
483
|
+
}
|
|
484
|
+
const goal = { ...this.goal, status: "active" };
|
|
485
|
+
return await this.runLogicalTurn(async (logicalTurn) => {
|
|
486
|
+
await this.setGoal(goal);
|
|
487
|
+
try {
|
|
488
|
+
if (logicalTurn.cancellationRequested) {
|
|
489
|
+
await this.blockActiveGoal();
|
|
490
|
+
return { turn: abortedTurnOutcome() };
|
|
491
|
+
}
|
|
492
|
+
const { userHistoryEntryId } = await this.record({
|
|
493
|
+
text: buildGoalContinuationText(goal),
|
|
494
|
+
});
|
|
495
|
+
if (logicalTurn.cancellationRequested) {
|
|
496
|
+
return { turn: await this.cancelLogicalTurn(userHistoryEntryId) };
|
|
497
|
+
}
|
|
498
|
+
return { turn: await this.runTurnNow(logicalTurn, userHistoryEntryId) };
|
|
499
|
+
}
|
|
500
|
+
catch (error) {
|
|
501
|
+
await this.blockActiveGoal().catch(() => undefined);
|
|
502
|
+
throw error;
|
|
503
|
+
}
|
|
504
|
+
});
|
|
505
|
+
}
|
|
506
|
+
async clearGoal() {
|
|
507
|
+
this.assertActive();
|
|
508
|
+
if (!this.goal) {
|
|
509
|
+
throw new Error("no goal exists");
|
|
510
|
+
}
|
|
511
|
+
await this.setGoal(null);
|
|
512
|
+
return await this.snapshot();
|
|
513
|
+
}
|
|
400
514
|
async record(options) {
|
|
401
515
|
this.assertActive();
|
|
402
516
|
const userHistoryEntryId = await this.session.commitUserText(options.text, options.historyEntryId ? { historyEntryId: options.historyEntryId } : undefined);
|
|
@@ -406,51 +520,118 @@ class LocalHostedSessionHandle {
|
|
|
406
520
|
};
|
|
407
521
|
}
|
|
408
522
|
async runTurn() {
|
|
523
|
+
return await this.runLogicalTurn((logicalTurn) => this.runTurnNow(logicalTurn));
|
|
524
|
+
}
|
|
525
|
+
async retryTurn() {
|
|
526
|
+
const userMessage = this.session.rawHistoryEntries.findLast((entry) => entry.message.role === "user");
|
|
527
|
+
if (userMessage && hasGoalTurnMetadata(userMessage.message)) {
|
|
528
|
+
throw new SessionRetryUnavailableError("goal-controlled turns cannot be retried; resume a blocked goal or start a new goal");
|
|
529
|
+
}
|
|
530
|
+
return await this.runTurn();
|
|
531
|
+
}
|
|
532
|
+
async runLogicalTurn(execute) {
|
|
409
533
|
this.assertActive();
|
|
410
|
-
|
|
534
|
+
if (this.activeLogicalTurn) {
|
|
535
|
+
throw new Error("session turn is already active");
|
|
536
|
+
}
|
|
537
|
+
const logicalTurn = {
|
|
538
|
+
cancellationRequested: false,
|
|
539
|
+
pendingSteering: [],
|
|
540
|
+
};
|
|
541
|
+
this.activeLogicalTurn = logicalTurn;
|
|
542
|
+
const run = execute(logicalTurn);
|
|
411
543
|
this.activeTurnPromise = run;
|
|
412
544
|
try {
|
|
413
545
|
return await run;
|
|
414
546
|
}
|
|
415
547
|
finally {
|
|
416
548
|
if (this.activeTurnPromise === run) {
|
|
549
|
+
this.rejectBufferedLogicalSteering(logicalTurn, new Error("steering was not applied before the logical turn ended"));
|
|
417
550
|
this.activeTurnPromise = undefined;
|
|
551
|
+
this.activeLogicalTurn = undefined;
|
|
552
|
+
await this.emitSnapshotResetIfChanged("assistant-message");
|
|
418
553
|
}
|
|
419
554
|
}
|
|
420
555
|
}
|
|
421
|
-
async runTurnNow() {
|
|
422
|
-
const
|
|
423
|
-
|
|
556
|
+
async runTurnNow(logicalTurn, rootHistoryEntryId) {
|
|
557
|
+
const rootUserMessage = rootHistoryEntryId
|
|
558
|
+
? this.session.rawHistoryEntries.find((entry) => entry.id === rootHistoryEntryId && entry.message.role === "user")
|
|
559
|
+
: this.session.rawHistoryEntries.findLast((entry) => entry.message.role === "user");
|
|
560
|
+
if (!rootUserMessage) {
|
|
424
561
|
throw new Error("cannot run a turn without a user message");
|
|
425
562
|
}
|
|
426
|
-
let
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
if (result.aborted && this.draftAssistantMessage) {
|
|
431
|
-
await this.interruptDraftAssistantMessage();
|
|
563
|
+
let goalRootHistoryEntryId = this.goal?.status === "active" ? rootUserMessage.id : undefined;
|
|
564
|
+
while (true) {
|
|
565
|
+
if (logicalTurn.cancellationRequested) {
|
|
566
|
+
return await this.cancelLogicalTurn(rootUserMessage.id);
|
|
432
567
|
}
|
|
433
|
-
const
|
|
434
|
-
this.turnOutcomes.set(userMessage.id, outcome);
|
|
435
|
-
await this.emitSnapshotResetIfChanged("assistant-message");
|
|
436
|
-
return outcome;
|
|
437
|
-
}
|
|
438
|
-
catch (error) {
|
|
568
|
+
const committedSteering = [];
|
|
439
569
|
try {
|
|
440
|
-
|
|
570
|
+
while (logicalTurn.pendingSteering.length > 0) {
|
|
571
|
+
committedSteering.push(await this.commitBufferedLogicalSteering(logicalTurn));
|
|
572
|
+
}
|
|
573
|
+
if (logicalTurn.cancellationRequested) {
|
|
574
|
+
return await this.cancelLogicalTurn(rootUserMessage.id, committedSteering);
|
|
575
|
+
}
|
|
576
|
+
const userMessage = this.session.rawHistoryEntries.findLast((entry) => entry.message.role === "user");
|
|
577
|
+
if (!userMessage) {
|
|
578
|
+
throw new Error("cannot run a turn without a user message");
|
|
579
|
+
}
|
|
580
|
+
const result = await this.runtime.runTurn();
|
|
581
|
+
const terminalResult = result.terminalResult;
|
|
582
|
+
if (terminalResult.aborted && this.draftAssistantMessage) {
|
|
583
|
+
await this.interruptDraftAssistantMessage();
|
|
584
|
+
}
|
|
585
|
+
const initialOutcome = turnOutcomeFromResult(result, result.finalMessage);
|
|
586
|
+
const terminalOutcome = turnOutcomeFromResult(terminalResult, terminalResult.finalMessage);
|
|
587
|
+
this.turnOutcomes.set(userMessage.id, initialOutcome);
|
|
588
|
+
for (const steering of committedSteering) {
|
|
589
|
+
this.turnOutcomes.set(steering.historyEntryId, initialOutcome);
|
|
590
|
+
}
|
|
591
|
+
if (this.goal?.status === "active") {
|
|
592
|
+
goalRootHistoryEntryId ??= rootUserMessage.id;
|
|
593
|
+
}
|
|
594
|
+
if (goalRootHistoryEntryId) {
|
|
595
|
+
this.turnOutcomes.set(goalRootHistoryEntryId, terminalOutcome);
|
|
596
|
+
}
|
|
597
|
+
if (terminalOutcome.status !== "completed") {
|
|
598
|
+
await this.blockActiveGoal();
|
|
599
|
+
}
|
|
600
|
+
await this.emitSnapshotResetIfChanged("assistant-message");
|
|
601
|
+
this.resolveCommittedLogicalSteering(committedSteering, initialOutcome);
|
|
602
|
+
if (logicalTurn.cancellationRequested) {
|
|
603
|
+
return await this.cancelLogicalTurn(rootUserMessage.id);
|
|
604
|
+
}
|
|
605
|
+
if (terminalOutcome.status !== "completed" || this.goal?.status !== "active") {
|
|
606
|
+
return goalRootHistoryEntryId ? terminalOutcome : initialOutcome;
|
|
607
|
+
}
|
|
608
|
+
if (logicalTurn.pendingSteering.length > 0) {
|
|
609
|
+
continue;
|
|
610
|
+
}
|
|
611
|
+
await this.session.commitUserText(buildGoalContinuationText(this.goal));
|
|
441
612
|
}
|
|
442
|
-
catch {
|
|
443
|
-
|
|
613
|
+
catch (error) {
|
|
614
|
+
this.rejectCommittedLogicalSteering(committedSteering, error);
|
|
615
|
+
await this.cleanupFailedTurn().catch(() => undefined);
|
|
616
|
+
await this.blockActiveGoal().catch(() => undefined);
|
|
617
|
+
throw error;
|
|
444
618
|
}
|
|
445
|
-
throw error;
|
|
446
619
|
}
|
|
447
620
|
}
|
|
448
621
|
interruptTurn() {
|
|
449
622
|
this.assertActive();
|
|
450
|
-
|
|
623
|
+
const logicalTurn = this.activeLogicalTurn;
|
|
624
|
+
if (logicalTurn) {
|
|
625
|
+
logicalTurn.cancellationRequested = true;
|
|
626
|
+
}
|
|
627
|
+
return this.runtime.interruptTurn() || Boolean(logicalTurn);
|
|
451
628
|
}
|
|
452
629
|
interruptActiveWork() {
|
|
453
|
-
|
|
630
|
+
const logicalTurn = this.activeLogicalTurn;
|
|
631
|
+
if (logicalTurn) {
|
|
632
|
+
logicalTurn.cancellationRequested = true;
|
|
633
|
+
}
|
|
634
|
+
let interrupted = this.runtime.interruptTurn() || Boolean(logicalTurn);
|
|
454
635
|
for (const abortController of this.activeWorkAbortControllers) {
|
|
455
636
|
if (!abortController.signal.aborted) {
|
|
456
637
|
abortController.abort();
|
|
@@ -476,30 +657,43 @@ class LocalHostedSessionHandle {
|
|
|
476
657
|
}
|
|
477
658
|
steer(text) {
|
|
478
659
|
this.assertActive();
|
|
479
|
-
if (
|
|
660
|
+
if (this.runtime.isTurnRunning) {
|
|
661
|
+
const submission = this.runtime.steer(text);
|
|
662
|
+
return {
|
|
663
|
+
id: submission.id,
|
|
664
|
+
applied: submission.applied.then((association) => ({
|
|
665
|
+
userHistoryEntryId: association.historyEntryId,
|
|
666
|
+
})),
|
|
667
|
+
result: submission.result.then(async (association) => {
|
|
668
|
+
const turn = turnOutcomeFromResult(association.result, association.result.finalMessage);
|
|
669
|
+
await this.commitSteeringTurnOutcome(association.historyEntryId, turn);
|
|
670
|
+
return {
|
|
671
|
+
userHistoryEntryId: association.historyEntryId,
|
|
672
|
+
turn,
|
|
673
|
+
};
|
|
674
|
+
}),
|
|
675
|
+
};
|
|
676
|
+
}
|
|
677
|
+
const logicalTurn = this.activeLogicalTurn;
|
|
678
|
+
if (!logicalTurn || this.goal?.status !== "active") {
|
|
480
679
|
throw new Error("cannot steer without an active turn");
|
|
481
680
|
}
|
|
482
|
-
|
|
483
|
-
return {
|
|
484
|
-
id: submission.id,
|
|
485
|
-
applied: submission.applied.then(async (association) => {
|
|
486
|
-
await this.commitSnapshot();
|
|
487
|
-
return { userHistoryEntryId: association.historyEntryId };
|
|
488
|
-
}),
|
|
489
|
-
result: submission.result.then(async (association) => {
|
|
490
|
-
const turn = turnOutcomeFromResult(association.result, association.result.finalMessage);
|
|
491
|
-
this.turnOutcomes.set(association.historyEntryId, turn);
|
|
492
|
-
await this.emitSnapshotResetIfChanged("assistant-message");
|
|
493
|
-
return {
|
|
494
|
-
userHistoryEntryId: association.historyEntryId,
|
|
495
|
-
turn,
|
|
496
|
-
};
|
|
497
|
-
}),
|
|
498
|
-
};
|
|
681
|
+
return this.bufferLogicalSteering(logicalTurn, text);
|
|
499
682
|
}
|
|
500
683
|
cancelSteering() {
|
|
501
684
|
this.assertActive();
|
|
502
|
-
|
|
685
|
+
const cancelled = this.runtime.cancelSteering();
|
|
686
|
+
const logicalTurn = this.activeLogicalTurn;
|
|
687
|
+
if (!logicalTurn) {
|
|
688
|
+
return cancelled;
|
|
689
|
+
}
|
|
690
|
+
const buffered = logicalTurn.pendingSteering.splice(0);
|
|
691
|
+
const error = new Error("steering submission was cancelled");
|
|
692
|
+
for (const submission of buffered) {
|
|
693
|
+
submission.rejectApplied(error);
|
|
694
|
+
submission.rejectResult(error);
|
|
695
|
+
}
|
|
696
|
+
return [...cancelled, ...buffered.map(({ id, text }) => ({ id, text }))];
|
|
503
697
|
}
|
|
504
698
|
async exec(options) {
|
|
505
699
|
return await this.runExec(options.execId, options.signal, async (signal) => {
|
|
@@ -875,6 +1069,120 @@ class LocalHostedSessionHandle {
|
|
|
875
1069
|
throw new AggregateError(errors, "failed to dispose hosted session");
|
|
876
1070
|
}
|
|
877
1071
|
}
|
|
1072
|
+
buildNewGoal(objective) {
|
|
1073
|
+
this.assertActive();
|
|
1074
|
+
if (this.goal) {
|
|
1075
|
+
throw new Error("a goal already exists; clear it before creating another");
|
|
1076
|
+
}
|
|
1077
|
+
const normalized = objective.trim();
|
|
1078
|
+
if (!normalized) {
|
|
1079
|
+
throw new Error("goal objective must not be empty");
|
|
1080
|
+
}
|
|
1081
|
+
return { objective: normalized, status: "active" };
|
|
1082
|
+
}
|
|
1083
|
+
async setGoal(goal) {
|
|
1084
|
+
const previousGoal = structuredClone(this.goal);
|
|
1085
|
+
this.goal = structuredClone(goal);
|
|
1086
|
+
try {
|
|
1087
|
+
await this.emitPatch("goal", [{ type: "goal.set", goal: structuredClone(goal) }]);
|
|
1088
|
+
}
|
|
1089
|
+
catch (error) {
|
|
1090
|
+
this.goal = previousGoal;
|
|
1091
|
+
throw error;
|
|
1092
|
+
}
|
|
1093
|
+
}
|
|
1094
|
+
async blockActiveGoal() {
|
|
1095
|
+
if (this.goal?.status === "active") {
|
|
1096
|
+
await this.setGoal({ ...this.goal, status: "blocked" });
|
|
1097
|
+
}
|
|
1098
|
+
}
|
|
1099
|
+
bufferLogicalSteering(logicalTurn, text) {
|
|
1100
|
+
const normalized = text.trim();
|
|
1101
|
+
if (!normalized) {
|
|
1102
|
+
throw new Error("steering input must not be empty");
|
|
1103
|
+
}
|
|
1104
|
+
let resolveApplied;
|
|
1105
|
+
let rejectApplied;
|
|
1106
|
+
const applied = new Promise((resolve, reject) => {
|
|
1107
|
+
resolveApplied = resolve;
|
|
1108
|
+
rejectApplied = reject;
|
|
1109
|
+
});
|
|
1110
|
+
let resolveResult;
|
|
1111
|
+
let rejectResult;
|
|
1112
|
+
const result = new Promise((resolve, reject) => {
|
|
1113
|
+
resolveResult = resolve;
|
|
1114
|
+
rejectResult = reject;
|
|
1115
|
+
});
|
|
1116
|
+
const submission = {
|
|
1117
|
+
id: `logical-steering-${randomUUID()}`,
|
|
1118
|
+
text: normalized,
|
|
1119
|
+
applied,
|
|
1120
|
+
result,
|
|
1121
|
+
resolveApplied,
|
|
1122
|
+
resolveResult,
|
|
1123
|
+
rejectApplied,
|
|
1124
|
+
rejectResult,
|
|
1125
|
+
};
|
|
1126
|
+
logicalTurn.pendingSteering.push(submission);
|
|
1127
|
+
return { id: submission.id, applied, result };
|
|
1128
|
+
}
|
|
1129
|
+
async commitBufferedLogicalSteering(logicalTurn) {
|
|
1130
|
+
const submissions = logicalTurn.pendingSteering.splice(0);
|
|
1131
|
+
if (submissions.length === 0) {
|
|
1132
|
+
throw new Error("cannot commit empty logical steering");
|
|
1133
|
+
}
|
|
1134
|
+
try {
|
|
1135
|
+
const goal = this.goal;
|
|
1136
|
+
if (goal?.status !== "active") {
|
|
1137
|
+
throw new Error("cannot commit logical steering without an active goal");
|
|
1138
|
+
}
|
|
1139
|
+
const historyEntryId = await this.session.commitUserText(prependGoalPolicy(formatSteeringUserMessage(submissions.map((submission) => submission.text)), goal));
|
|
1140
|
+
for (const submission of submissions) {
|
|
1141
|
+
submission.resolveApplied({ userHistoryEntryId: historyEntryId });
|
|
1142
|
+
}
|
|
1143
|
+
return { historyEntryId, submissions };
|
|
1144
|
+
}
|
|
1145
|
+
catch (error) {
|
|
1146
|
+
const steeringError = error instanceof Error ? error : new Error(String(error));
|
|
1147
|
+
for (const submission of submissions) {
|
|
1148
|
+
submission.rejectApplied(steeringError);
|
|
1149
|
+
submission.rejectResult(steeringError);
|
|
1150
|
+
}
|
|
1151
|
+
throw error;
|
|
1152
|
+
}
|
|
1153
|
+
}
|
|
1154
|
+
resolveCommittedLogicalSteering(committed, turn) {
|
|
1155
|
+
for (const { historyEntryId, submissions } of committed) {
|
|
1156
|
+
for (const submission of submissions) {
|
|
1157
|
+
submission.resolveResult({ userHistoryEntryId: historyEntryId, turn });
|
|
1158
|
+
}
|
|
1159
|
+
}
|
|
1160
|
+
}
|
|
1161
|
+
rejectCommittedLogicalSteering(committed, error) {
|
|
1162
|
+
const steeringError = error instanceof Error ? error : new Error(String(error));
|
|
1163
|
+
for (const { submissions } of committed) {
|
|
1164
|
+
for (const submission of submissions) {
|
|
1165
|
+
submission.rejectResult(steeringError);
|
|
1166
|
+
}
|
|
1167
|
+
}
|
|
1168
|
+
}
|
|
1169
|
+
rejectBufferedLogicalSteering(logicalTurn, error) {
|
|
1170
|
+
for (const submission of logicalTurn.pendingSteering.splice(0)) {
|
|
1171
|
+
submission.rejectApplied(error);
|
|
1172
|
+
submission.rejectResult(error);
|
|
1173
|
+
}
|
|
1174
|
+
}
|
|
1175
|
+
async cancelLogicalTurn(rootHistoryEntryId, committedSteering = []) {
|
|
1176
|
+
const outcome = abortedTurnOutcome();
|
|
1177
|
+
this.turnOutcomes.set(rootHistoryEntryId, outcome);
|
|
1178
|
+
for (const steering of committedSteering) {
|
|
1179
|
+
this.turnOutcomes.set(steering.historyEntryId, outcome);
|
|
1180
|
+
}
|
|
1181
|
+
await this.blockActiveGoal();
|
|
1182
|
+
await this.emitSnapshotResetIfChanged("assistant-message");
|
|
1183
|
+
this.resolveCommittedLogicalSteering(committedSteering, outcome);
|
|
1184
|
+
return outcome;
|
|
1185
|
+
}
|
|
878
1186
|
async commitSnapshot() {
|
|
879
1187
|
const write = this.snapshotQueue.catch(() => undefined).then(() => this.writeSnapshot());
|
|
880
1188
|
this.snapshotQueue = write.catch(() => undefined);
|
|
@@ -1024,7 +1332,8 @@ class LocalHostedSessionHandle {
|
|
|
1024
1332
|
? { usageCheckpoint: { ...agentState.usageCheckpoint } }
|
|
1025
1333
|
: {}),
|
|
1026
1334
|
},
|
|
1027
|
-
lifecycle: this.runtime.isTurnRunning ? "running" : "idle",
|
|
1335
|
+
lifecycle: this.activeTurnPromise || this.runtime.isTurnRunning ? "running" : "idle",
|
|
1336
|
+
goal: structuredClone(this.goal),
|
|
1028
1337
|
costTotal: this.costTotal,
|
|
1029
1338
|
settings: {
|
|
1030
1339
|
personaId: this.runtime.persona.id,
|
|
@@ -1178,6 +1487,28 @@ class LocalHostedSessionHandle {
|
|
|
1178
1487
|
this.runtimeEventQueue = write.catch(() => undefined);
|
|
1179
1488
|
return write;
|
|
1180
1489
|
}
|
|
1490
|
+
async commitSteeringTurnOutcome(historyEntryId, turn) {
|
|
1491
|
+
const write = this.runtimeEventQueue
|
|
1492
|
+
.catch(() => undefined)
|
|
1493
|
+
.then(async () => {
|
|
1494
|
+
this.turnOutcomes.set(historyEntryId, turn);
|
|
1495
|
+
const message = this.committedSnapshot?.messages.find((candidate) => candidate.id === historyEntryId);
|
|
1496
|
+
if (!message) {
|
|
1497
|
+
throw new Error(`missing steering user message '${historyEntryId}'`);
|
|
1498
|
+
}
|
|
1499
|
+
if (isDeepStrictEqual(message.turn, turn)) {
|
|
1500
|
+
return;
|
|
1501
|
+
}
|
|
1502
|
+
await this.emitPatch("assistant-message", [
|
|
1503
|
+
{
|
|
1504
|
+
type: "message.replace",
|
|
1505
|
+
message: { ...structuredClone(message), turn },
|
|
1506
|
+
},
|
|
1507
|
+
]);
|
|
1508
|
+
});
|
|
1509
|
+
this.runtimeEventQueue = write.catch(() => undefined);
|
|
1510
|
+
await write;
|
|
1511
|
+
}
|
|
1181
1512
|
removeToolRun(tool, changes) {
|
|
1182
1513
|
this.tools.delete(tool.id);
|
|
1183
1514
|
for (const facetId of tool.facetIds) {
|
|
@@ -1310,9 +1641,13 @@ class LocalHostedSessionHandle {
|
|
|
1310
1641
|
});
|
|
1311
1642
|
return;
|
|
1312
1643
|
}
|
|
1313
|
-
case "user_message":
|
|
1644
|
+
case "user_message": {
|
|
1645
|
+
const pendingGoal = this.pendingGoalCommit;
|
|
1314
1646
|
await this.emitPatch("user-message", [
|
|
1315
1647
|
this.agentStateChange(),
|
|
1648
|
+
...(pendingGoal
|
|
1649
|
+
? [{ type: "goal.set", goal: structuredClone(pendingGoal) }]
|
|
1650
|
+
: []),
|
|
1316
1651
|
{
|
|
1317
1652
|
type: "message.append",
|
|
1318
1653
|
message: {
|
|
@@ -1324,7 +1659,11 @@ class LocalHostedSessionHandle {
|
|
|
1324
1659
|
timelineItem: timelineItemForMessage(event.historyEntryId),
|
|
1325
1660
|
},
|
|
1326
1661
|
], { persist: true });
|
|
1662
|
+
if (pendingGoal && this.pendingGoalCommit === pendingGoal) {
|
|
1663
|
+
this.pendingGoalCommit = undefined;
|
|
1664
|
+
}
|
|
1327
1665
|
return;
|
|
1666
|
+
}
|
|
1328
1667
|
case "history_rewound":
|
|
1329
1668
|
this.reconcileProjections({ removeMissingAgents: true });
|
|
1330
1669
|
await this.emitSnapshotResetIfChanged("maintenance");
|
|
@@ -1718,6 +2057,9 @@ class LocalHostedSessionHandle {
|
|
|
1718
2057
|
await this.emitSnapshotResetIfChanged("assistant-message");
|
|
1719
2058
|
}
|
|
1720
2059
|
}
|
|
2060
|
+
function abortedTurnOutcome() {
|
|
2061
|
+
return { status: "aborted", stopReason: "aborted" };
|
|
2062
|
+
}
|
|
1721
2063
|
function turnOutcomeFromResult(result, assistantMessage) {
|
|
1722
2064
|
if (result.blocked) {
|
|
1723
2065
|
return { status: "blocked", ...result.blocked };
|
|
@@ -1806,6 +2148,10 @@ function normalizeRecoveredSnapshot(snapshot) {
|
|
|
1806
2148
|
const legacyAgentState = recovered.agentState.contextEpoch === LEGACY_SESSION_CONTEXT_EPOCH;
|
|
1807
2149
|
let changed = recovered.lifecycle !== "idle" || legacyAgentState;
|
|
1808
2150
|
recovered.lifecycle = "idle";
|
|
2151
|
+
if (recovered.goal?.status === "active") {
|
|
2152
|
+
changed = true;
|
|
2153
|
+
recovered.goal.status = "blocked";
|
|
2154
|
+
}
|
|
1809
2155
|
if (Object.keys(recovered.agents).length > 0) {
|
|
1810
2156
|
changed = true;
|
|
1811
2157
|
recovered.agents = {};
|