tinker-agent 2.6.0 → 2.8.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/CHANGELOG.md +40 -1
- package/package.json +1 -1
- package/src/agent/loop.ts +22 -0
- package/src/agent/runtime-session.ts +174 -0
- package/src/cli/tui-runner.tsx +14 -1
- package/src/events/stdout-event-printer.ts +14 -0
- package/src/events/types.ts +10 -1
- package/src/memory/contracts.ts +46 -0
- package/src/memory/memory-coordinator.ts +445 -4
- package/src/memory/memory-create-tool.ts +117 -0
- package/src/memory/memory-delete-tool.ts +88 -0
- package/src/memory/memory-store.ts +239 -0
- package/src/memory/memory-update-tool.ts +142 -0
- package/src/observation/observation-builder.ts +70 -0
- package/src/session/session-tool-result-codec.ts +4 -0
- package/src/tools/ask-user.ts +100 -0
- package/src/tools/bash.ts +1 -1
- package/src/tools/context-maintenance.ts +1 -1
- package/src/tools/read.ts +1 -1
- package/src/tools/registry.ts +30 -0
- package/src/tools/types.ts +71 -0
- package/src/tools/wait.ts +1 -3
- package/src/tui/app.tsx +33 -5
- package/src/tui/components/ask-user.tsx +61 -0
- package/src/tui/components/footer.tsx +14 -1
- package/src/tui/components/resume-session-picker.tsx +118 -37
- package/src/tui/event-store.ts +57 -0
- package/src/tui/tui-session-controller.ts +8 -0
|
@@ -4,18 +4,25 @@ import type {
|
|
|
4
4
|
CompletedTurnHookFailure,
|
|
5
5
|
CompletedTurnHookInput,
|
|
6
6
|
} from "../agent/runtime-session";
|
|
7
|
+
import { throwIfTurnCancelled } from "../agent/turn-cancellation";
|
|
8
|
+
import type { ToolCall } from "../agent/types";
|
|
7
9
|
import type { SessionId } from "../ids/runtime-id";
|
|
8
10
|
import type { ModelContextBudget } from "../model/model-context-profile";
|
|
9
11
|
import type { ModelClient } from "../model/model-client";
|
|
10
12
|
import type { CompletedTurnSnapshot } from "../session/session-store";
|
|
11
13
|
import type {
|
|
12
14
|
MemoryGetRawResult,
|
|
15
|
+
MemoryCreateRawResult,
|
|
16
|
+
MemoryDeleteRawResult,
|
|
13
17
|
MemorySearchRawResult,
|
|
18
|
+
MemoryUpdateRawResult,
|
|
14
19
|
ToolExecutor,
|
|
15
20
|
} from "../tools/types";
|
|
16
21
|
import {
|
|
17
22
|
boundedMemoryError,
|
|
18
23
|
MAX_SEARCH_RESULT_SUMMARY_BYTES,
|
|
24
|
+
MEMORY_CREATE_TOOL_NAME,
|
|
25
|
+
MEMORY_DELETE_TOOL_NAME,
|
|
19
26
|
MEMORY_EXTRACTION_QUEUE_CAPACITY,
|
|
20
27
|
memoryErrorCode,
|
|
21
28
|
MEMORY_GET_TOOL_NAME,
|
|
@@ -24,6 +31,7 @@ import {
|
|
|
24
31
|
MEMORY_SEARCH_DIAGNOSTIC_VECTOR_SCORES_LIMIT,
|
|
25
32
|
MEMORY_SEARCH_LIMIT,
|
|
26
33
|
MEMORY_SEARCH_TOOL_NAME,
|
|
34
|
+
MEMORY_UPDATE_TOOL_NAME,
|
|
27
35
|
MemoryError,
|
|
28
36
|
boundedMemoryErrorDetail,
|
|
29
37
|
truncateUtf8,
|
|
@@ -33,6 +41,7 @@ import {
|
|
|
33
41
|
type MemoryFtsMatch,
|
|
34
42
|
type MemoryGetDiagnostic,
|
|
35
43
|
type MemoryHybridMatch,
|
|
44
|
+
type MemoryMutationDiagnostic,
|
|
36
45
|
type MemoryPaths,
|
|
37
46
|
type MemoryRecallDegraded,
|
|
38
47
|
type MemoryRecallPath,
|
|
@@ -53,7 +62,10 @@ import {
|
|
|
53
62
|
} from "./memory-extractor";
|
|
54
63
|
import { ExtractedMemoryLog, MemoryLog } from "./memory-log";
|
|
55
64
|
import { createMemoryGetToolExecutor } from "./memory-get-tool";
|
|
65
|
+
import { createMemoryCreateToolExecutor } from "./memory-create-tool";
|
|
66
|
+
import { createMemoryDeleteToolExecutor } from "./memory-delete-tool";
|
|
56
67
|
import { createMemorySearchToolExecutor } from "./memory-search-tool";
|
|
68
|
+
import { createMemoryUpdateToolExecutor } from "./memory-update-tool";
|
|
57
69
|
import { MemoryStore, resolveMemoryPaths } from "./memory-store";
|
|
58
70
|
import { normalizeEmbedding } from "./vector";
|
|
59
71
|
|
|
@@ -70,6 +82,19 @@ type ActiveMemoryTask = {
|
|
|
70
82
|
completion?: Promise<void>;
|
|
71
83
|
};
|
|
72
84
|
|
|
85
|
+
type MemoryToolSource = {
|
|
86
|
+
readonly workspaceRoot: string;
|
|
87
|
+
readonly sessionId: SessionId;
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
const MEMORY_TOOL_NAMES: readonly string[] = Object.freeze([
|
|
91
|
+
MEMORY_SEARCH_TOOL_NAME,
|
|
92
|
+
MEMORY_GET_TOOL_NAME,
|
|
93
|
+
MEMORY_CREATE_TOOL_NAME,
|
|
94
|
+
MEMORY_UPDATE_TOOL_NAME,
|
|
95
|
+
MEMORY_DELETE_TOOL_NAME,
|
|
96
|
+
]);
|
|
97
|
+
|
|
73
98
|
export type CreateMemoryCoordinatorInput = {
|
|
74
99
|
readonly paths?: MemoryPaths;
|
|
75
100
|
readonly embedding: MemoryEmbeddingConfig;
|
|
@@ -183,6 +208,30 @@ export class MemoryCoordinator implements CompletedTurnHook {
|
|
|
183
208
|
});
|
|
184
209
|
}
|
|
185
210
|
|
|
211
|
+
createCreateToolExecutor(input: MemoryToolSource): ToolExecutor {
|
|
212
|
+
return createMemoryCreateToolExecutor({
|
|
213
|
+
create: (text, summary, call, signal) =>
|
|
214
|
+
this.createMemory(text, summary, call, signal, input),
|
|
215
|
+
recordInvalidCall: (call) => this.recordInvalidMutation("create", call, input),
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
createUpdateToolExecutor(input: MemoryToolSource): ToolExecutor {
|
|
220
|
+
return createMemoryUpdateToolExecutor({
|
|
221
|
+
update: (memoryId, text, summary, call, signal) =>
|
|
222
|
+
this.updateMemory(memoryId, text, summary, call, signal, input),
|
|
223
|
+
recordInvalidCall: (call) => this.recordInvalidMutation("update", call, input),
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
createDeleteToolExecutor(input: MemoryToolSource): ToolExecutor {
|
|
228
|
+
return createMemoryDeleteToolExecutor({
|
|
229
|
+
delete: (memoryId, call, signal) =>
|
|
230
|
+
this.deleteMemory(memoryId, call, signal, input),
|
|
231
|
+
recordInvalidCall: (call) => this.recordInvalidMutation("delete", call, input),
|
|
232
|
+
});
|
|
233
|
+
}
|
|
234
|
+
|
|
186
235
|
listStoredMemories(): readonly StoredMemorySummary[] {
|
|
187
236
|
return this.store.listStoredMemories();
|
|
188
237
|
}
|
|
@@ -411,6 +460,375 @@ export class MemoryCoordinator implements CompletedTurnHook {
|
|
|
411
460
|
}
|
|
412
461
|
}
|
|
413
462
|
|
|
463
|
+
private async createMemory(
|
|
464
|
+
text: string,
|
|
465
|
+
summary: string,
|
|
466
|
+
call: ToolCall,
|
|
467
|
+
signal: AbortSignal,
|
|
468
|
+
source: MemoryToolSource,
|
|
469
|
+
): Promise<MemoryCreateRawResult> {
|
|
470
|
+
const startedAt = performance.now();
|
|
471
|
+
try {
|
|
472
|
+
throwIfTurnCancelled(signal);
|
|
473
|
+
if (containsSensitiveMemory(text) || containsSensitiveMemory(summary)) {
|
|
474
|
+
await this.logMutation({
|
|
475
|
+
kind: "create",
|
|
476
|
+
outcome: "failed",
|
|
477
|
+
reason: "memory_sensitive_rejected",
|
|
478
|
+
source,
|
|
479
|
+
call,
|
|
480
|
+
memoryId: null,
|
|
481
|
+
startedAt,
|
|
482
|
+
});
|
|
483
|
+
return {
|
|
484
|
+
ok: false,
|
|
485
|
+
error:
|
|
486
|
+
"MemoryCreate rejected content that may contain sensitive information.",
|
|
487
|
+
};
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
let embedding: Float32Array;
|
|
491
|
+
try {
|
|
492
|
+
embedding = await this.embedMutationText(text, signal);
|
|
493
|
+
} catch (error) {
|
|
494
|
+
if (signal.aborted) {
|
|
495
|
+
throw error;
|
|
496
|
+
}
|
|
497
|
+
await this.logMutation({
|
|
498
|
+
kind: "create",
|
|
499
|
+
outcome: "failed",
|
|
500
|
+
reason: "memory_embedding_failed",
|
|
501
|
+
source,
|
|
502
|
+
call,
|
|
503
|
+
memoryId: null,
|
|
504
|
+
startedAt,
|
|
505
|
+
});
|
|
506
|
+
return {
|
|
507
|
+
ok: false,
|
|
508
|
+
error: "MemoryCreate could not generate a memory embedding.",
|
|
509
|
+
};
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
throwIfTurnCancelled(signal);
|
|
513
|
+
const result = this.store.insertBatch({
|
|
514
|
+
workspaceRoot: source.workspaceRoot,
|
|
515
|
+
sessionId: source.sessionId,
|
|
516
|
+
turnId: call.turnId,
|
|
517
|
+
candidates: [{ text, summary, embedding }],
|
|
518
|
+
});
|
|
519
|
+
const inserted = result.inserted[0];
|
|
520
|
+
const existing =
|
|
521
|
+
inserted === undefined ? this.store.getByTextHash(text) : undefined;
|
|
522
|
+
const resolved = inserted ?? existing;
|
|
523
|
+
if (resolved === undefined) {
|
|
524
|
+
throw new MemoryError(
|
|
525
|
+
"memory_write_failed",
|
|
526
|
+
"MemoryCreate did not produce a stored record.",
|
|
527
|
+
);
|
|
528
|
+
}
|
|
529
|
+
const memoryId = resolved.memoryId;
|
|
530
|
+
const createdAt = resolved.createdAt;
|
|
531
|
+
const status = inserted === undefined ? "already_exists" : "created";
|
|
532
|
+
await this.logMutation({
|
|
533
|
+
kind: "create",
|
|
534
|
+
outcome: "ok",
|
|
535
|
+
reason: null,
|
|
536
|
+
source,
|
|
537
|
+
call,
|
|
538
|
+
memoryId,
|
|
539
|
+
startedAt,
|
|
540
|
+
});
|
|
541
|
+
return { ok: true, status, memoryId, createdAt };
|
|
542
|
+
} catch (error) {
|
|
543
|
+
if (signal.aborted) {
|
|
544
|
+
await this.logMutation({
|
|
545
|
+
kind: "create",
|
|
546
|
+
outcome: "skipped",
|
|
547
|
+
reason: "memory_create_cancelled",
|
|
548
|
+
source,
|
|
549
|
+
call,
|
|
550
|
+
memoryId: null,
|
|
551
|
+
startedAt,
|
|
552
|
+
});
|
|
553
|
+
throw error;
|
|
554
|
+
}
|
|
555
|
+
await this.logMutation({
|
|
556
|
+
kind: "create",
|
|
557
|
+
outcome: "failed",
|
|
558
|
+
reason: memoryErrorCode(error, "memory_create_failed"),
|
|
559
|
+
source,
|
|
560
|
+
call,
|
|
561
|
+
memoryId: null,
|
|
562
|
+
startedAt,
|
|
563
|
+
});
|
|
564
|
+
return { ok: false, error: boundedMemoryError(error) };
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
private async updateMemory(
|
|
569
|
+
memoryId: string,
|
|
570
|
+
text: string,
|
|
571
|
+
summary: string,
|
|
572
|
+
call: ToolCall,
|
|
573
|
+
signal: AbortSignal,
|
|
574
|
+
source: MemoryToolSource,
|
|
575
|
+
): Promise<MemoryUpdateRawResult> {
|
|
576
|
+
const startedAt = performance.now();
|
|
577
|
+
try {
|
|
578
|
+
throwIfTurnCancelled(signal);
|
|
579
|
+
if (containsSensitiveMemory(text) || containsSensitiveMemory(summary)) {
|
|
580
|
+
await this.logMutation({
|
|
581
|
+
kind: "update",
|
|
582
|
+
outcome: "failed",
|
|
583
|
+
reason: "memory_sensitive_rejected",
|
|
584
|
+
source,
|
|
585
|
+
call,
|
|
586
|
+
memoryId,
|
|
587
|
+
startedAt,
|
|
588
|
+
});
|
|
589
|
+
return {
|
|
590
|
+
ok: false,
|
|
591
|
+
error:
|
|
592
|
+
"MemoryUpdate rejected content that may contain sensitive information.",
|
|
593
|
+
};
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
const target = this.store.getByIdForMutation(memoryId);
|
|
597
|
+
if (target === undefined) {
|
|
598
|
+
await this.logMutation({
|
|
599
|
+
kind: "update",
|
|
600
|
+
outcome: "failed",
|
|
601
|
+
reason: "memory_not_found",
|
|
602
|
+
source,
|
|
603
|
+
call,
|
|
604
|
+
memoryId,
|
|
605
|
+
startedAt,
|
|
606
|
+
});
|
|
607
|
+
return {
|
|
608
|
+
ok: false,
|
|
609
|
+
code: "memory_not_found",
|
|
610
|
+
error: `No global memory exists with id ${memoryId}.`,
|
|
611
|
+
};
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
let embedding = target.embedding;
|
|
615
|
+
if (text !== target.text) {
|
|
616
|
+
try {
|
|
617
|
+
embedding = await this.embedMutationText(text, signal);
|
|
618
|
+
} catch (error) {
|
|
619
|
+
if (signal.aborted) {
|
|
620
|
+
throw error;
|
|
621
|
+
}
|
|
622
|
+
await this.logMutation({
|
|
623
|
+
kind: "update",
|
|
624
|
+
outcome: "failed",
|
|
625
|
+
reason: "memory_embedding_failed",
|
|
626
|
+
source,
|
|
627
|
+
call,
|
|
628
|
+
memoryId,
|
|
629
|
+
startedAt,
|
|
630
|
+
});
|
|
631
|
+
return {
|
|
632
|
+
ok: false,
|
|
633
|
+
error: "MemoryUpdate could not generate a memory embedding.",
|
|
634
|
+
};
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
throwIfTurnCancelled(signal);
|
|
639
|
+
const result = this.store.updateMemory({
|
|
640
|
+
memoryId,
|
|
641
|
+
text,
|
|
642
|
+
summary,
|
|
643
|
+
embedding,
|
|
644
|
+
});
|
|
645
|
+
if (!result.ok && result.code === "memory_not_found") {
|
|
646
|
+
await this.logMutation({
|
|
647
|
+
kind: "update",
|
|
648
|
+
outcome: "failed",
|
|
649
|
+
reason: "memory_not_found",
|
|
650
|
+
source,
|
|
651
|
+
call,
|
|
652
|
+
memoryId,
|
|
653
|
+
startedAt,
|
|
654
|
+
});
|
|
655
|
+
return {
|
|
656
|
+
ok: false,
|
|
657
|
+
code: "memory_not_found",
|
|
658
|
+
error: `No global memory exists with id ${memoryId}.`,
|
|
659
|
+
};
|
|
660
|
+
}
|
|
661
|
+
if (!result.ok && result.code === "memory_duplicate") {
|
|
662
|
+
await this.logMutation({
|
|
663
|
+
kind: "update",
|
|
664
|
+
outcome: "failed",
|
|
665
|
+
reason: "memory_duplicate",
|
|
666
|
+
source,
|
|
667
|
+
call,
|
|
668
|
+
memoryId,
|
|
669
|
+
startedAt,
|
|
670
|
+
});
|
|
671
|
+
return {
|
|
672
|
+
ok: false,
|
|
673
|
+
code: "memory_duplicate",
|
|
674
|
+
conflictMemoryId: result.conflictMemoryId,
|
|
675
|
+
error: `Another global memory already has the replacement text (memoryId ${result.conflictMemoryId}).`,
|
|
676
|
+
};
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
await this.logMutation({
|
|
680
|
+
kind: "update",
|
|
681
|
+
outcome: "ok",
|
|
682
|
+
reason: null,
|
|
683
|
+
source,
|
|
684
|
+
call,
|
|
685
|
+
memoryId,
|
|
686
|
+
startedAt,
|
|
687
|
+
});
|
|
688
|
+
return { ok: true, status: "updated", memoryId };
|
|
689
|
+
} catch (error) {
|
|
690
|
+
if (signal.aborted) {
|
|
691
|
+
await this.logMutation({
|
|
692
|
+
kind: "update",
|
|
693
|
+
outcome: "skipped",
|
|
694
|
+
reason: "memory_update_cancelled",
|
|
695
|
+
source,
|
|
696
|
+
call,
|
|
697
|
+
memoryId,
|
|
698
|
+
startedAt,
|
|
699
|
+
});
|
|
700
|
+
throw error;
|
|
701
|
+
}
|
|
702
|
+
await this.logMutation({
|
|
703
|
+
kind: "update",
|
|
704
|
+
outcome: "failed",
|
|
705
|
+
reason: memoryErrorCode(error, "memory_update_failed"),
|
|
706
|
+
source,
|
|
707
|
+
call,
|
|
708
|
+
memoryId,
|
|
709
|
+
startedAt,
|
|
710
|
+
});
|
|
711
|
+
return { ok: false, error: boundedMemoryError(error) };
|
|
712
|
+
}
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
private async deleteMemory(
|
|
716
|
+
memoryId: string,
|
|
717
|
+
call: ToolCall,
|
|
718
|
+
signal: AbortSignal,
|
|
719
|
+
source: MemoryToolSource,
|
|
720
|
+
): Promise<MemoryDeleteRawResult> {
|
|
721
|
+
const startedAt = performance.now();
|
|
722
|
+
try {
|
|
723
|
+
throwIfTurnCancelled(signal);
|
|
724
|
+
const result = this.store.deleteMemory(memoryId);
|
|
725
|
+
if (!result.ok) {
|
|
726
|
+
await this.logMutation({
|
|
727
|
+
kind: "delete",
|
|
728
|
+
outcome: "failed",
|
|
729
|
+
reason: "memory_not_found",
|
|
730
|
+
source,
|
|
731
|
+
call,
|
|
732
|
+
memoryId,
|
|
733
|
+
startedAt,
|
|
734
|
+
});
|
|
735
|
+
return {
|
|
736
|
+
ok: false,
|
|
737
|
+
code: "memory_not_found",
|
|
738
|
+
error: `No global memory exists with id ${memoryId}.`,
|
|
739
|
+
};
|
|
740
|
+
}
|
|
741
|
+
await this.logMutation({
|
|
742
|
+
kind: "delete",
|
|
743
|
+
outcome: "ok",
|
|
744
|
+
reason: null,
|
|
745
|
+
source,
|
|
746
|
+
call,
|
|
747
|
+
memoryId,
|
|
748
|
+
startedAt,
|
|
749
|
+
});
|
|
750
|
+
return { ok: true, status: "deleted", memoryId };
|
|
751
|
+
} catch (error) {
|
|
752
|
+
if (signal.aborted) {
|
|
753
|
+
await this.logMutation({
|
|
754
|
+
kind: "delete",
|
|
755
|
+
outcome: "skipped",
|
|
756
|
+
reason: "memory_delete_cancelled",
|
|
757
|
+
source,
|
|
758
|
+
call,
|
|
759
|
+
memoryId,
|
|
760
|
+
startedAt,
|
|
761
|
+
});
|
|
762
|
+
throw error;
|
|
763
|
+
}
|
|
764
|
+
await this.logMutation({
|
|
765
|
+
kind: "delete",
|
|
766
|
+
outcome: "failed",
|
|
767
|
+
reason: memoryErrorCode(error, "memory_delete_failed"),
|
|
768
|
+
source,
|
|
769
|
+
call,
|
|
770
|
+
memoryId,
|
|
771
|
+
startedAt,
|
|
772
|
+
});
|
|
773
|
+
return { ok: false, error: boundedMemoryError(error) };
|
|
774
|
+
}
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
private async embedMutationText(
|
|
778
|
+
text: string,
|
|
779
|
+
signal: AbortSignal,
|
|
780
|
+
): Promise<Float32Array> {
|
|
781
|
+
const raw = await this.embeddingClient.embed([text], signal);
|
|
782
|
+
if (raw.length !== 1 || raw[0] === undefined) {
|
|
783
|
+
throw new MemoryError(
|
|
784
|
+
"memory_embedding_response_invalid",
|
|
785
|
+
"Embedding response did not contain the memory vector.",
|
|
786
|
+
);
|
|
787
|
+
}
|
|
788
|
+
return normalizeEmbedding(raw[0], this.embeddingDimensions);
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
private recordInvalidMutation(
|
|
792
|
+
kind: MemoryMutationDiagnostic["kind"],
|
|
793
|
+
call: ToolCall,
|
|
794
|
+
source: MemoryToolSource,
|
|
795
|
+
): Promise<void> {
|
|
796
|
+
return this.logMutation({
|
|
797
|
+
kind,
|
|
798
|
+
outcome: "failed",
|
|
799
|
+
reason: `memory_${kind}_args_invalid`,
|
|
800
|
+
source,
|
|
801
|
+
call,
|
|
802
|
+
memoryId: null,
|
|
803
|
+
startedAt: performance.now(),
|
|
804
|
+
});
|
|
805
|
+
}
|
|
806
|
+
|
|
807
|
+
private logMutation(input: {
|
|
808
|
+
readonly kind: MemoryMutationDiagnostic["kind"];
|
|
809
|
+
readonly outcome: MemoryMutationDiagnostic["outcome"];
|
|
810
|
+
readonly reason: string | null;
|
|
811
|
+
readonly source: MemoryToolSource;
|
|
812
|
+
readonly call: ToolCall;
|
|
813
|
+
readonly memoryId: string | null;
|
|
814
|
+
readonly startedAt: number;
|
|
815
|
+
}): Promise<void> {
|
|
816
|
+
return this.log.append(
|
|
817
|
+
mutationDiagnostic({
|
|
818
|
+
clock: this.clock,
|
|
819
|
+
kind: input.kind,
|
|
820
|
+
outcome: input.outcome,
|
|
821
|
+
reason: input.reason,
|
|
822
|
+
workspace: input.source.workspaceRoot,
|
|
823
|
+
sessionId: input.source.sessionId,
|
|
824
|
+
turnId: input.call.turnId,
|
|
825
|
+
toolCallId: input.call.toolCallId,
|
|
826
|
+
memoryId: input.memoryId,
|
|
827
|
+
ms: elapsedMs(input.startedAt),
|
|
828
|
+
}),
|
|
829
|
+
);
|
|
830
|
+
}
|
|
831
|
+
|
|
414
832
|
private async search(
|
|
415
833
|
query: string | null,
|
|
416
834
|
keywords: readonly string[],
|
|
@@ -631,10 +1049,7 @@ export function buildExtractionEvidenceText(
|
|
|
631
1049
|
}
|
|
632
1050
|
const messages = snapshot.messages
|
|
633
1051
|
.filter(
|
|
634
|
-
(message) =>
|
|
635
|
-
message.role !== "tool" ||
|
|
636
|
-
(message.name !== MEMORY_SEARCH_TOOL_NAME &&
|
|
637
|
-
message.name !== MEMORY_GET_TOOL_NAME),
|
|
1052
|
+
(message) => message.role !== "tool" || !MEMORY_TOOL_NAMES.includes(message.name),
|
|
638
1053
|
)
|
|
639
1054
|
.map((message) => ({ ...message }));
|
|
640
1055
|
return JSON.stringify(
|
|
@@ -742,6 +1157,32 @@ function getDiagnostic(input: {
|
|
|
742
1157
|
});
|
|
743
1158
|
}
|
|
744
1159
|
|
|
1160
|
+
function mutationDiagnostic(input: {
|
|
1161
|
+
readonly clock: () => string;
|
|
1162
|
+
readonly kind: MemoryMutationDiagnostic["kind"];
|
|
1163
|
+
readonly outcome: MemoryMutationDiagnostic["outcome"];
|
|
1164
|
+
readonly reason: string | null;
|
|
1165
|
+
readonly workspace: string;
|
|
1166
|
+
readonly sessionId: string;
|
|
1167
|
+
readonly turnId: string;
|
|
1168
|
+
readonly toolCallId: string;
|
|
1169
|
+
readonly memoryId: string | null;
|
|
1170
|
+
readonly ms: number;
|
|
1171
|
+
}): MemoryMutationDiagnostic {
|
|
1172
|
+
return Object.freeze({
|
|
1173
|
+
at: input.clock(),
|
|
1174
|
+
kind: input.kind,
|
|
1175
|
+
outcome: input.outcome,
|
|
1176
|
+
reason: input.reason,
|
|
1177
|
+
workspace: input.workspace,
|
|
1178
|
+
sessionId: input.sessionId,
|
|
1179
|
+
turnId: input.turnId,
|
|
1180
|
+
toolCallId: input.toolCallId,
|
|
1181
|
+
memoryId: input.memoryId,
|
|
1182
|
+
ms: input.ms,
|
|
1183
|
+
});
|
|
1184
|
+
}
|
|
1185
|
+
|
|
745
1186
|
function emptyRejectedCounts(): MemoryExtractionRejectedCounts {
|
|
746
1187
|
return Object.freeze({
|
|
747
1188
|
duplicate: 0,
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import { throwIfTurnCancelled } from "../agent/turn-cancellation";
|
|
2
|
+
import type { ToolCall } from "../agent/types";
|
|
3
|
+
import {
|
|
4
|
+
defineToolExecutor,
|
|
5
|
+
type MemoryCreateRawResult,
|
|
6
|
+
type ToolDefinition,
|
|
7
|
+
type ToolExecutor,
|
|
8
|
+
} from "../tools/types";
|
|
9
|
+
import {
|
|
10
|
+
MAX_MEMORY_SUMMARY_BYTES,
|
|
11
|
+
MAX_MEMORY_TEXT_BYTES,
|
|
12
|
+
MEMORY_CREATE_TOOL_NAME,
|
|
13
|
+
} from "./contracts";
|
|
14
|
+
|
|
15
|
+
export const MEMORY_CREATE_TOOL_DEFINITION: ToolDefinition = Object.freeze({
|
|
16
|
+
name: MEMORY_CREATE_TOOL_NAME,
|
|
17
|
+
description: "Create one global memory shared across sessions and workspaces.",
|
|
18
|
+
parameters: {
|
|
19
|
+
type: "object",
|
|
20
|
+
additionalProperties: false,
|
|
21
|
+
properties: {
|
|
22
|
+
text: {
|
|
23
|
+
type: "string",
|
|
24
|
+
minLength: 1,
|
|
25
|
+
maxLength: MAX_MEMORY_TEXT_BYTES,
|
|
26
|
+
description: "A one-line searchable index for this memory.",
|
|
27
|
+
},
|
|
28
|
+
summary: {
|
|
29
|
+
type: "string",
|
|
30
|
+
maxLength: MAX_MEMORY_SUMMARY_BYTES,
|
|
31
|
+
description:
|
|
32
|
+
"Optional details, reasons, constraints, and scope for the memory.",
|
|
33
|
+
},
|
|
34
|
+
},
|
|
35
|
+
required: ["text"],
|
|
36
|
+
},
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
export function createMemoryCreateToolExecutor(options: {
|
|
40
|
+
readonly create: (
|
|
41
|
+
text: string,
|
|
42
|
+
summary: string,
|
|
43
|
+
call: ToolCall,
|
|
44
|
+
signal: AbortSignal,
|
|
45
|
+
) => Promise<MemoryCreateRawResult>;
|
|
46
|
+
readonly recordInvalidCall: (call: ToolCall) => Promise<void>;
|
|
47
|
+
}): ToolExecutor {
|
|
48
|
+
return defineToolExecutor("memory_create", {
|
|
49
|
+
definition: MEMORY_CREATE_TOOL_DEFINITION,
|
|
50
|
+
async execute(args, call, context): Promise<MemoryCreateRawResult> {
|
|
51
|
+
const parsed = parseMemoryCreateArgs(args);
|
|
52
|
+
if (!parsed.ok) {
|
|
53
|
+
throwIfTurnCancelled(context.signal);
|
|
54
|
+
await options.recordInvalidCall(call);
|
|
55
|
+
throwIfTurnCancelled(context.signal);
|
|
56
|
+
return { ok: false, error: parsed.error };
|
|
57
|
+
}
|
|
58
|
+
const result = await options.create(
|
|
59
|
+
parsed.text,
|
|
60
|
+
parsed.summary,
|
|
61
|
+
call,
|
|
62
|
+
context.signal,
|
|
63
|
+
);
|
|
64
|
+
throwIfTurnCancelled(context.signal);
|
|
65
|
+
return result;
|
|
66
|
+
},
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
type ParsedMemoryCreateArgs =
|
|
71
|
+
| { readonly ok: true; readonly text: string; readonly summary: string }
|
|
72
|
+
| { readonly ok: false; readonly error: string };
|
|
73
|
+
|
|
74
|
+
function parseMemoryCreateArgs(args: unknown): ParsedMemoryCreateArgs {
|
|
75
|
+
if (!isRecord(args)) {
|
|
76
|
+
return {
|
|
77
|
+
ok: false,
|
|
78
|
+
error:
|
|
79
|
+
"MemoryCreate arguments must be an object containing only text and summary.",
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
const unexpected = Object.keys(args).find(
|
|
83
|
+
(key) => key !== "text" && key !== "summary",
|
|
84
|
+
);
|
|
85
|
+
if (unexpected !== undefined) {
|
|
86
|
+
return {
|
|
87
|
+
ok: false,
|
|
88
|
+
error: `MemoryCreate received unexpected field: ${unexpected}.`,
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
if (typeof args.text !== "string") {
|
|
92
|
+
return { ok: false, error: "MemoryCreate.text must be a string." };
|
|
93
|
+
}
|
|
94
|
+
const text = args.text.trim();
|
|
95
|
+
const textBytes = Buffer.byteLength(text, "utf8");
|
|
96
|
+
if (textBytes < 1 || textBytes > MAX_MEMORY_TEXT_BYTES) {
|
|
97
|
+
return {
|
|
98
|
+
ok: false,
|
|
99
|
+
error: `MemoryCreate.text must be 1 to ${MAX_MEMORY_TEXT_BYTES} UTF-8 bytes after trimming.`,
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
if (args.summary !== undefined && typeof args.summary !== "string") {
|
|
103
|
+
return { ok: false, error: "MemoryCreate.summary must be a string." };
|
|
104
|
+
}
|
|
105
|
+
const summary = typeof args.summary === "string" ? args.summary.trim() : "";
|
|
106
|
+
if (Buffer.byteLength(summary, "utf8") > MAX_MEMORY_SUMMARY_BYTES) {
|
|
107
|
+
return {
|
|
108
|
+
ok: false,
|
|
109
|
+
error: `MemoryCreate.summary must be at most ${MAX_MEMORY_SUMMARY_BYTES} UTF-8 bytes after trimming.`,
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
return { ok: true, text, summary };
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
116
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
117
|
+
}
|