tinker-agent 1.0.65 → 1.2.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 +36 -0
- package/LICENSE +201 -0
- package/NOTICE +6 -0
- package/README.md +90 -9
- package/bin/tinker.js +39 -0
- package/node_modules/ansi-regex/index.d.ts +33 -0
- package/node_modules/ansi-regex/index.js +14 -0
- package/node_modules/ansi-regex/license +9 -0
- package/node_modules/ansi-regex/package.json +61 -0
- package/node_modules/ansi-regex/readme.md +66 -0
- package/node_modules/ansi-styles/index.d.ts +236 -0
- package/node_modules/ansi-styles/index.js +223 -0
- package/node_modules/ansi-styles/license +9 -0
- package/node_modules/ansi-styles/package.json +54 -0
- package/node_modules/ansi-styles/readme.md +173 -0
- package/node_modules/chalk/license +9 -0
- package/node_modules/chalk/package.json +83 -0
- package/node_modules/chalk/readme.md +297 -0
- package/node_modules/chalk/source/index.d.ts +325 -0
- package/node_modules/chalk/source/index.js +225 -0
- package/node_modules/chalk/source/utilities.js +33 -0
- package/node_modules/chalk/source/vendor/ansi-styles/index.d.ts +236 -0
- package/node_modules/chalk/source/vendor/ansi-styles/index.js +223 -0
- package/node_modules/chalk/source/vendor/supports-color/browser.d.ts +1 -0
- package/node_modules/chalk/source/vendor/supports-color/browser.js +34 -0
- package/node_modules/chalk/source/vendor/supports-color/index.d.ts +55 -0
- package/node_modules/chalk/source/vendor/supports-color/index.js +190 -0
- package/node_modules/character-entities/index.d.ts +6 -0
- package/node_modules/character-entities/index.js +2132 -0
- package/node_modules/character-entities/license +22 -0
- package/node_modules/character-entities/package.json +78 -0
- package/node_modules/character-entities/readme.md +152 -0
- package/node_modules/decode-named-character-reference/index.d.ts +13 -0
- package/node_modules/decode-named-character-reference/index.d.ts.map +1 -0
- package/node_modules/decode-named-character-reference/index.dom.d.ts +6 -0
- package/node_modules/decode-named-character-reference/index.dom.d.ts.map +1 -0
- package/node_modules/decode-named-character-reference/index.dom.js +32 -0
- package/node_modules/decode-named-character-reference/index.js +19 -0
- package/node_modules/decode-named-character-reference/license +22 -0
- package/node_modules/decode-named-character-reference/package.json +90 -0
- package/node_modules/decode-named-character-reference/readme.md +136 -0
- package/node_modules/get-east-asian-width/index.d.ts +60 -0
- package/node_modules/get-east-asian-width/index.js +30 -0
- package/node_modules/get-east-asian-width/license +9 -0
- package/node_modules/get-east-asian-width/lookup-data.js +21 -0
- package/node_modules/get-east-asian-width/lookup.js +138 -0
- package/node_modules/get-east-asian-width/package.json +71 -0
- package/node_modules/get-east-asian-width/readme.md +65 -0
- package/node_modules/get-east-asian-width/utilities.js +24 -0
- package/node_modules/has-flag/index.d.ts +39 -0
- package/node_modules/has-flag/index.js +8 -0
- package/node_modules/has-flag/license +9 -0
- package/node_modules/has-flag/package.json +49 -0
- package/node_modules/has-flag/readme.md +74 -0
- package/node_modules/is-fullwidth-code-point/index.d.ts +17 -0
- package/node_modules/is-fullwidth-code-point/index.js +12 -0
- package/node_modules/is-fullwidth-code-point/license +9 -0
- package/node_modules/is-fullwidth-code-point/package.json +53 -0
- package/node_modules/is-fullwidth-code-point/readme.md +31 -0
- package/node_modules/markdansi/LICENSE +21 -0
- package/node_modules/markdansi/README.md +167 -0
- package/node_modules/markdansi/dist/ast.d.ts +99 -0
- package/node_modules/markdansi/dist/ast.js +1 -0
- package/node_modules/markdansi/dist/cli.d.ts +17 -0
- package/node_modules/markdansi/dist/cli.js +208 -0
- package/node_modules/markdansi/dist/hyperlink.d.ts +9 -0
- package/node_modules/markdansi/dist/hyperlink.js +26 -0
- package/node_modules/markdansi/dist/index.d.ts +10 -0
- package/node_modules/markdansi/dist/index.js +16 -0
- package/node_modules/markdansi/dist/parser.d.ts +2 -0
- package/node_modules/markdansi/dist/parser.js +205 -0
- package/node_modules/markdansi/dist/render.d.ts +9 -0
- package/node_modules/markdansi/dist/render.js +680 -0
- package/node_modules/markdansi/dist/stream.d.ts +39 -0
- package/node_modules/markdansi/dist/stream.js +192 -0
- package/node_modules/markdansi/dist/theme.d.ts +18 -0
- package/node_modules/markdansi/dist/theme.js +105 -0
- package/node_modules/markdansi/dist/types.d.ts +58 -0
- package/node_modules/markdansi/dist/types.js +1 -0
- package/node_modules/markdansi/dist/wrap.d.ts +10 -0
- package/node_modules/markdansi/dist/wrap.js +73 -0
- package/node_modules/markdansi/docs/spec.md +110 -0
- package/node_modules/markdansi/package.json +78 -0
- package/node_modules/markdansi/tsconfig.json +18 -0
- package/node_modules/marked/LICENSE +44 -0
- package/node_modules/marked/README.md +115 -0
- package/node_modules/marked/bin/main.js +284 -0
- package/node_modules/marked/bin/marked.js +16 -0
- package/node_modules/marked/lib/marked.d.ts +760 -0
- package/node_modules/marked/lib/marked.esm.js +77 -0
- package/node_modules/marked/lib/marked.esm.js.map +7 -0
- package/node_modules/marked/lib/marked.umd.js +79 -0
- package/node_modules/marked/lib/marked.umd.js.map +7 -0
- package/node_modules/marked/man/marked.1 +113 -0
- package/node_modules/marked/man/marked.1.md +93 -0
- package/node_modules/marked/package.json +103 -0
- package/node_modules/slice-ansi/index.d.ts +19 -0
- package/node_modules/slice-ansi/index.js +317 -0
- package/node_modules/slice-ansi/license +10 -0
- package/node_modules/slice-ansi/package.json +59 -0
- package/node_modules/slice-ansi/readme.md +55 -0
- package/node_modules/slice-ansi/tokenize-ansi.js +752 -0
- package/node_modules/string-width/index.d.ts +39 -0
- package/node_modules/string-width/index.js +203 -0
- package/node_modules/string-width/license +9 -0
- package/node_modules/string-width/package.json +65 -0
- package/node_modules/string-width/readme.md +66 -0
- package/node_modules/strip-ansi/index.d.ts +15 -0
- package/node_modules/strip-ansi/index.js +19 -0
- package/node_modules/strip-ansi/license +9 -0
- package/node_modules/strip-ansi/package.json +59 -0
- package/node_modules/strip-ansi/readme.md +37 -0
- package/node_modules/supports-color/browser.d.ts +1 -0
- package/node_modules/supports-color/browser.js +35 -0
- package/node_modules/supports-color/index.d.ts +55 -0
- package/node_modules/supports-color/index.js +202 -0
- package/node_modules/supports-color/license +9 -0
- package/node_modules/supports-color/package.json +64 -0
- package/node_modules/supports-color/readme.md +75 -0
- package/node_modules/supports-hyperlinks/browser.js +10 -0
- package/node_modules/supports-hyperlinks/index.d.ts +25 -0
- package/node_modules/supports-hyperlinks/index.js +163 -0
- package/node_modules/supports-hyperlinks/license +10 -0
- package/node_modules/supports-hyperlinks/package.json +47 -0
- package/node_modules/supports-hyperlinks/readme.md +45 -0
- package/package.json +41 -10
- package/patches/markdansi@0.3.2.patch +22 -2
- package/src/agent/context-builder.ts +24 -10
- package/src/agent/context-meter.ts +89 -5
- package/src/agent/loop.ts +107 -10
- package/src/agent/runtime-session.ts +1437 -82
- package/src/agent/session-ledger.ts +169 -29
- package/src/agent/types.ts +12 -1
- package/src/agent/user-prompt-projection.ts +62 -0
- package/src/cli/config.ts +57 -14
- package/src/cli/model-profiles.ts +124 -0
- package/src/cli/run-runner.ts +4 -1
- package/src/cli/tui-runner.tsx +68 -30
- package/src/context/compiled-context-hash.ts +1 -1
- package/src/context/compiled-context-validator.ts +92 -15
- package/src/context/context-automation-policy.ts +121 -0
- package/src/context/context-manager.ts +390 -11
- package/src/context/context-policy.ts +8 -0
- package/src/context/context-protocol-validator.ts +11 -1
- package/src/context/context-revision-compiler.ts +190 -27
- package/src/context/context-revision.ts +105 -19
- package/src/context/context-surface.ts +329 -0
- package/src/context/prefix-retirement-planner.ts +570 -0
- package/src/context/protocol-frame.ts +30 -3
- package/src/context/recall-retirement-contract.ts +27 -0
- package/src/context/swap-planner.ts +34 -10
- package/src/events/observation-text-log.ts +3 -3
- package/src/events/stdout-event-printer.ts +39 -3
- package/src/events/types.ts +172 -35
- package/src/ids/runtime-id.ts +6 -0
- package/src/ids/uuid-v7.ts +7 -0
- package/src/image/image-asset-store.ts +319 -0
- package/src/image/image-input-policy.ts +14 -0
- package/src/image/image-probe.ts +233 -0
- package/src/image/image-types.ts +262 -0
- package/src/instructions/project-instructions.ts +4 -1
- package/src/mcp/mcp-manager.ts +83 -19
- package/src/model/fake-model-client.ts +9 -1
- package/src/model/input-token-estimator.ts +25 -0
- package/src/model/model-client.ts +60 -0
- package/src/model/model-request-preflight.ts +1 -0
- package/src/model/moonshot-input-token-estimator.ts +107 -0
- package/src/model/openai-chat-mapping.ts +77 -1
- package/src/model/openai-chat-model-client.ts +389 -59
- package/src/model/openai-chat-stream.ts +272 -0
- package/src/model/prompt-prefix-hash.ts +2 -4
- package/src/model/token-estimator.ts +4 -0
- package/src/observation/observation-builder.ts +29 -0
- package/src/session/resume-projection.ts +94 -34
- package/src/session/session-catalog.ts +4 -4
- package/src/session/session-errors.ts +1 -1
- package/src/session/session-history-reader.ts +151 -4
- package/src/session/session-last-response-reader.ts +189 -0
- package/src/session/session-schema.ts +349 -35
- package/src/session/session-store.ts +3266 -255
- package/src/session/sqlite-session-ledger.ts +9 -4
- package/src/skills/skill-catalog.ts +182 -0
- package/src/skills/skill-context.ts +364 -0
- package/src/skills/skill-loader.ts +779 -0
- package/src/skills/skill-tool.ts +297 -0
- package/src/tools/edit.ts +51 -64
- package/src/tools/ensure-parent-directory.ts +6 -0
- package/src/tools/read.ts +2 -2
- package/src/tools/recall.ts +43 -40
- package/src/tools/registry.ts +24 -3
- package/src/tools/ripgrep.ts +3 -2
- package/src/tools/types.ts +43 -4
- package/src/tools/write.ts +20 -31
- package/src/tui/app.tsx +333 -96
- package/src/tui/clipboard.ts +5 -0
- package/src/tui/components/mcp-panel.tsx +28 -0
- package/src/tui/components/prompt-input.tsx +790 -257
- package/src/tui/components/skills-panel.tsx +31 -0
- package/src/tui/components/timeline.tsx +30 -1
- package/src/tui/event-store.ts +85 -2
- package/src/tui/file-mention.ts +234 -0
- package/src/tui/line-editor.ts +1 -0
- package/src/tui/project-slash-commands.ts +229 -0
- package/src/tui/prompt-draft.ts +510 -0
- package/src/tui/prompt-history.ts +294 -22
- package/src/tui/slash-commands.ts +46 -2
- package/src/tui/tui-session-controller.ts +110 -48
- package/src/tui/workspace-file-search.ts +94 -0
|
@@ -1,23 +1,66 @@
|
|
|
1
1
|
import { appendFile, mkdir, readFile } from "node:fs/promises";
|
|
2
2
|
import path from "node:path";
|
|
3
|
+
import type { RuntimeIdFactory } from "../ids/runtime-id";
|
|
4
|
+
import {
|
|
5
|
+
parseImageAssetId,
|
|
6
|
+
parseImageAttachmentId,
|
|
7
|
+
type ImageAttachmentId,
|
|
8
|
+
} from "../image/image-types";
|
|
9
|
+
import { stableJsonStringify } from "../model/model-request-preflight";
|
|
10
|
+
import {
|
|
11
|
+
createPromptDraft,
|
|
12
|
+
validatePromptDraft,
|
|
13
|
+
type DraftImageAttachment,
|
|
14
|
+
type PromptDraft,
|
|
15
|
+
type PromptElement,
|
|
16
|
+
} from "./prompt-draft";
|
|
3
17
|
|
|
4
18
|
const DEFAULT_MAX_ENTRIES = 200;
|
|
5
19
|
|
|
20
|
+
export type PromptHistoryEntry =
|
|
21
|
+
| { readonly version: 1; readonly text: string }
|
|
22
|
+
| {
|
|
23
|
+
readonly version: 2;
|
|
24
|
+
readonly text: string;
|
|
25
|
+
readonly elements: readonly PromptElement[];
|
|
26
|
+
readonly attachments: readonly DraftImageAttachment[];
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
export type LoadedPromptHistoryRecord =
|
|
30
|
+
| {
|
|
31
|
+
readonly kind: "valid";
|
|
32
|
+
readonly lineNumber: number;
|
|
33
|
+
readonly entry: PromptHistoryEntry;
|
|
34
|
+
}
|
|
35
|
+
| {
|
|
36
|
+
readonly kind: "invalid";
|
|
37
|
+
readonly lineNumber: number;
|
|
38
|
+
readonly errorCode: "INVALID_JSON" | "UNSUPPORTED_VERSION" | "INVALID_ENTRY";
|
|
39
|
+
};
|
|
40
|
+
|
|
6
41
|
export type PromptHistoryOptions = {
|
|
7
42
|
filePath?: string;
|
|
8
43
|
entries?: string[];
|
|
44
|
+
records?: LoadedPromptHistoryRecord[];
|
|
9
45
|
maxEntries?: number;
|
|
10
46
|
};
|
|
11
47
|
|
|
12
48
|
export class PromptHistory {
|
|
13
49
|
readonly filePath?: string;
|
|
14
50
|
private readonly maxEntries: number;
|
|
15
|
-
private items:
|
|
51
|
+
private items: LoadedPromptHistoryRecord[];
|
|
16
52
|
|
|
17
53
|
constructor(options: PromptHistoryOptions = {}) {
|
|
18
54
|
this.filePath = options.filePath;
|
|
19
55
|
this.maxEntries = options.maxEntries ?? DEFAULT_MAX_ENTRIES;
|
|
20
|
-
|
|
56
|
+
const records =
|
|
57
|
+
options.records ??
|
|
58
|
+
(options.entries ?? []).map((text, index) => ({
|
|
59
|
+
kind: "valid" as const,
|
|
60
|
+
lineNumber: index + 1,
|
|
61
|
+
entry: Object.freeze({ version: 1 as const, text }),
|
|
62
|
+
}));
|
|
63
|
+
this.items = trimToMax(records, this.maxEntries);
|
|
21
64
|
}
|
|
22
65
|
|
|
23
66
|
static async load(
|
|
@@ -34,56 +77,285 @@ export class PromptHistory {
|
|
|
34
77
|
throw error;
|
|
35
78
|
}
|
|
36
79
|
|
|
37
|
-
const
|
|
38
|
-
for (const line of content.split("\n")) {
|
|
80
|
+
const records: LoadedPromptHistoryRecord[] = [];
|
|
81
|
+
for (const [index, line] of content.split("\n").entries()) {
|
|
39
82
|
if (line.trim() === "") {
|
|
40
83
|
continue;
|
|
41
84
|
}
|
|
42
|
-
|
|
43
|
-
const parsed = parsePromptLine(line);
|
|
44
|
-
if (parsed !== undefined) {
|
|
45
|
-
entries.push(parsed);
|
|
46
|
-
}
|
|
85
|
+
records.push(parsePromptLine(line, index + 1));
|
|
47
86
|
}
|
|
87
|
+
return new PromptHistory({
|
|
88
|
+
filePath,
|
|
89
|
+
records,
|
|
90
|
+
maxEntries: options.maxEntries,
|
|
91
|
+
});
|
|
92
|
+
}
|
|
48
93
|
|
|
49
|
-
|
|
94
|
+
get records(): readonly LoadedPromptHistoryRecord[] {
|
|
95
|
+
return this.items;
|
|
50
96
|
}
|
|
51
97
|
|
|
52
98
|
get entries(): readonly string[] {
|
|
53
|
-
return this.items
|
|
99
|
+
return this.items.flatMap((record) =>
|
|
100
|
+
record.kind === "valid" ? [record.entry.text] : [],
|
|
101
|
+
);
|
|
54
102
|
}
|
|
55
103
|
|
|
56
|
-
async append(prompt: string): Promise<void> {
|
|
57
|
-
|
|
104
|
+
async append(prompt: string | PromptDraft): Promise<void> {
|
|
105
|
+
const entry =
|
|
106
|
+
typeof prompt === "string"
|
|
107
|
+
? Object.freeze({ version: 1 as const, text: prompt })
|
|
108
|
+
: historyEntryFromDraft(prompt);
|
|
109
|
+
if (entry.text === "") {
|
|
58
110
|
return;
|
|
59
111
|
}
|
|
60
|
-
|
|
61
|
-
|
|
112
|
+
const previous = this.items.at(-1);
|
|
113
|
+
if (
|
|
114
|
+
previous?.kind === "valid" &&
|
|
115
|
+
historySemanticKey(previous.entry) === historySemanticKey(entry)
|
|
116
|
+
) {
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
const lineNumber = (this.items.at(-1)?.lineNumber ?? 0) + 1;
|
|
120
|
+
this.items = trimToMax(
|
|
121
|
+
[...this.items, { kind: "valid", lineNumber, entry }],
|
|
122
|
+
this.maxEntries,
|
|
123
|
+
);
|
|
62
124
|
|
|
63
125
|
if (this.filePath === undefined) {
|
|
64
126
|
return;
|
|
65
127
|
}
|
|
66
|
-
|
|
67
128
|
await mkdir(path.dirname(this.filePath), { recursive: true });
|
|
68
|
-
|
|
129
|
+
const serialized =
|
|
130
|
+
entry.version === 1 ? JSON.stringify(entry.text) : stableJsonStringify(entry);
|
|
131
|
+
await appendFile(this.filePath, `${serialized}\n`, "utf8");
|
|
69
132
|
}
|
|
70
133
|
}
|
|
71
134
|
|
|
72
|
-
function
|
|
135
|
+
export function restorePromptHistoryEntry(
|
|
136
|
+
entry: PromptHistoryEntry,
|
|
137
|
+
idFactory: Pick<RuntimeIdFactory, "createImageAttachmentId">,
|
|
138
|
+
): PromptDraft {
|
|
139
|
+
if (entry.version === 1) {
|
|
140
|
+
return createPromptDraft(entry.text);
|
|
141
|
+
}
|
|
142
|
+
const remap = new Map<ImageAttachmentId, ImageAttachmentId>();
|
|
143
|
+
for (const attachment of entry.attachments) {
|
|
144
|
+
remap.set(attachment.attachmentId, idFactory.createImageAttachmentId());
|
|
145
|
+
}
|
|
146
|
+
const elements = entry.elements.map((element) =>
|
|
147
|
+
Object.freeze({
|
|
148
|
+
...element,
|
|
149
|
+
attachmentId: requireRemapped(remap, element.attachmentId),
|
|
150
|
+
range: Object.freeze({ ...element.range }),
|
|
151
|
+
}),
|
|
152
|
+
);
|
|
153
|
+
const attachments = entry.attachments.map((attachment) =>
|
|
154
|
+
Object.freeze({
|
|
155
|
+
...attachment,
|
|
156
|
+
attachmentId: requireRemapped(remap, attachment.attachmentId),
|
|
157
|
+
asset: Object.freeze({ ...attachment.asset }),
|
|
158
|
+
}),
|
|
159
|
+
);
|
|
160
|
+
const draft = Object.freeze({
|
|
161
|
+
editor: Object.freeze({
|
|
162
|
+
value: entry.text,
|
|
163
|
+
cursor: [...entry.text].length,
|
|
164
|
+
}),
|
|
165
|
+
elements: Object.freeze(elements),
|
|
166
|
+
attachments: Object.freeze(attachments),
|
|
167
|
+
});
|
|
168
|
+
validatePromptDraft(draft);
|
|
169
|
+
return draft;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function parsePromptLine(line: string, lineNumber: number): LoadedPromptHistoryRecord {
|
|
173
|
+
let parsed: unknown;
|
|
174
|
+
try {
|
|
175
|
+
parsed = JSON.parse(line);
|
|
176
|
+
} catch {
|
|
177
|
+
return { kind: "invalid", lineNumber, errorCode: "INVALID_JSON" };
|
|
178
|
+
}
|
|
179
|
+
if (typeof parsed === "string") {
|
|
180
|
+
return parsed === ""
|
|
181
|
+
? { kind: "invalid", lineNumber, errorCode: "INVALID_ENTRY" }
|
|
182
|
+
: {
|
|
183
|
+
kind: "valid",
|
|
184
|
+
lineNumber,
|
|
185
|
+
entry: Object.freeze({ version: 1, text: parsed }),
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
if (!isRecord(parsed)) {
|
|
189
|
+
return { kind: "invalid", lineNumber, errorCode: "INVALID_ENTRY" };
|
|
190
|
+
}
|
|
191
|
+
if (parsed.version !== 2) {
|
|
192
|
+
return { kind: "invalid", lineNumber, errorCode: "UNSUPPORTED_VERSION" };
|
|
193
|
+
}
|
|
73
194
|
try {
|
|
74
|
-
const
|
|
75
|
-
return
|
|
195
|
+
const entry = decodeV2Entry(parsed);
|
|
196
|
+
return { kind: "valid", lineNumber, entry };
|
|
76
197
|
} catch {
|
|
77
|
-
return
|
|
198
|
+
return { kind: "invalid", lineNumber, errorCode: "INVALID_ENTRY" };
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function decodeV2Entry(record: Record<string, unknown>): PromptHistoryEntry {
|
|
203
|
+
assertKeys(record, ["version", "text", "elements", "attachments"]);
|
|
204
|
+
if (
|
|
205
|
+
typeof record.text !== "string" ||
|
|
206
|
+
!Array.isArray(record.elements) ||
|
|
207
|
+
!Array.isArray(record.attachments) ||
|
|
208
|
+
record.elements.length === 0
|
|
209
|
+
) {
|
|
210
|
+
throw new Error("Invalid Prompt history v2 entry.");
|
|
211
|
+
}
|
|
212
|
+
const elements = record.elements.map((value): PromptElement => {
|
|
213
|
+
const element = requireRecord(value);
|
|
214
|
+
assertKeys(element, ["kind", "attachmentId", "label", "range"]);
|
|
215
|
+
const range = requireRecord(element.range);
|
|
216
|
+
assertKeys(range, ["start", "end"]);
|
|
217
|
+
if (
|
|
218
|
+
element.kind !== "image" ||
|
|
219
|
+
typeof element.attachmentId !== "string" ||
|
|
220
|
+
typeof element.label !== "string" ||
|
|
221
|
+
!Number.isSafeInteger(range.start) ||
|
|
222
|
+
!Number.isSafeInteger(range.end)
|
|
223
|
+
) {
|
|
224
|
+
throw new Error("Invalid Prompt history image element.");
|
|
225
|
+
}
|
|
226
|
+
return Object.freeze({
|
|
227
|
+
kind: "image",
|
|
228
|
+
attachmentId: parseImageAttachmentId(element.attachmentId),
|
|
229
|
+
label: element.label,
|
|
230
|
+
range: Object.freeze({ start: range.start as number, end: range.end as number }),
|
|
231
|
+
});
|
|
232
|
+
});
|
|
233
|
+
const attachments = record.attachments.map((value): DraftImageAttachment => {
|
|
234
|
+
const attachment = requireRecord(value);
|
|
235
|
+
assertKeys(attachment, ["attachmentId", "asset", "originalName"]);
|
|
236
|
+
const asset = requireRecord(attachment.asset);
|
|
237
|
+
assertKeys(asset, ["assetId", "mimeType", "byteLength", "width", "height"]);
|
|
238
|
+
if (
|
|
239
|
+
typeof attachment.attachmentId !== "string" ||
|
|
240
|
+
typeof attachment.originalName !== "string" ||
|
|
241
|
+
typeof asset.assetId !== "string" ||
|
|
242
|
+
(asset.mimeType !== "image/png" &&
|
|
243
|
+
asset.mimeType !== "image/jpeg" &&
|
|
244
|
+
asset.mimeType !== "image/webp") ||
|
|
245
|
+
!Number.isSafeInteger(asset.byteLength) ||
|
|
246
|
+
!Number.isSafeInteger(asset.width) ||
|
|
247
|
+
!Number.isSafeInteger(asset.height)
|
|
248
|
+
) {
|
|
249
|
+
throw new Error("Invalid Prompt history image attachment.");
|
|
250
|
+
}
|
|
251
|
+
return Object.freeze({
|
|
252
|
+
attachmentId: parseImageAttachmentId(attachment.attachmentId),
|
|
253
|
+
asset: Object.freeze({
|
|
254
|
+
assetId: parseImageAssetId(asset.assetId),
|
|
255
|
+
mimeType: asset.mimeType,
|
|
256
|
+
byteLength: asset.byteLength as number,
|
|
257
|
+
width: asset.width as number,
|
|
258
|
+
height: asset.height as number,
|
|
259
|
+
}),
|
|
260
|
+
originalName: attachment.originalName,
|
|
261
|
+
});
|
|
262
|
+
});
|
|
263
|
+
const draft = Object.freeze({
|
|
264
|
+
editor: Object.freeze({
|
|
265
|
+
value: record.text,
|
|
266
|
+
cursor: [...record.text].length,
|
|
267
|
+
}),
|
|
268
|
+
elements: Object.freeze(elements),
|
|
269
|
+
attachments: Object.freeze(attachments),
|
|
270
|
+
});
|
|
271
|
+
validatePromptDraft(draft);
|
|
272
|
+
return historyEntryFromDraft(draft);
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
function historyEntryFromDraft(draft: PromptDraft): PromptHistoryEntry {
|
|
276
|
+
validatePromptDraft(draft);
|
|
277
|
+
if (draft.elements.length === 0) {
|
|
278
|
+
return Object.freeze({ version: 1, text: draft.editor.value });
|
|
78
279
|
}
|
|
280
|
+
return Object.freeze({
|
|
281
|
+
version: 2,
|
|
282
|
+
text: draft.editor.value,
|
|
283
|
+
elements: Object.freeze(
|
|
284
|
+
draft.elements.map((element) =>
|
|
285
|
+
Object.freeze({
|
|
286
|
+
...element,
|
|
287
|
+
range: Object.freeze({ ...element.range }),
|
|
288
|
+
}),
|
|
289
|
+
),
|
|
290
|
+
),
|
|
291
|
+
attachments: Object.freeze(
|
|
292
|
+
draft.attachments.map((attachment) =>
|
|
293
|
+
Object.freeze({
|
|
294
|
+
...attachment,
|
|
295
|
+
asset: Object.freeze({ ...attachment.asset }),
|
|
296
|
+
}),
|
|
297
|
+
),
|
|
298
|
+
),
|
|
299
|
+
});
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
function historySemanticKey(entry: PromptHistoryEntry): string {
|
|
303
|
+
return stableJsonStringify(
|
|
304
|
+
entry.version === 1
|
|
305
|
+
? entry
|
|
306
|
+
: {
|
|
307
|
+
version: 2,
|
|
308
|
+
text: entry.text,
|
|
309
|
+
elements: entry.elements.map((element) => {
|
|
310
|
+
const { attachmentId, ...semantic } = element;
|
|
311
|
+
void attachmentId;
|
|
312
|
+
return semantic;
|
|
313
|
+
}),
|
|
314
|
+
attachments: entry.attachments.map((attachment) => {
|
|
315
|
+
const { attachmentId, ...semantic } = attachment;
|
|
316
|
+
void attachmentId;
|
|
317
|
+
return semantic;
|
|
318
|
+
}),
|
|
319
|
+
},
|
|
320
|
+
);
|
|
79
321
|
}
|
|
80
322
|
|
|
81
|
-
function
|
|
323
|
+
function requireRemapped(
|
|
324
|
+
remap: ReadonlyMap<ImageAttachmentId, ImageAttachmentId>,
|
|
325
|
+
previous: ImageAttachmentId,
|
|
326
|
+
): ImageAttachmentId {
|
|
327
|
+
const value = remap.get(previous);
|
|
328
|
+
if (value === undefined) {
|
|
329
|
+
throw new Error("Prompt history attachment remap is incomplete.");
|
|
330
|
+
}
|
|
331
|
+
return value;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
function requireRecord(value: unknown): Record<string, unknown> {
|
|
335
|
+
if (!isRecord(value)) {
|
|
336
|
+
throw new Error("Expected an object.");
|
|
337
|
+
}
|
|
338
|
+
return value;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
function assertKeys(value: Record<string, unknown>, expected: readonly string[]): void {
|
|
342
|
+
const actual = Object.keys(value).sort();
|
|
343
|
+
const wanted = [...expected].sort();
|
|
344
|
+
if (stableJsonStringify(actual) !== stableJsonStringify(wanted)) {
|
|
345
|
+
throw new Error("Prompt history object keys are invalid.");
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
function trimToMax<T>(entries: T[], maxEntries: number): T[] {
|
|
82
350
|
return entries.length > maxEntries
|
|
83
351
|
? entries.slice(entries.length - maxEntries)
|
|
84
352
|
: entries;
|
|
85
353
|
}
|
|
86
354
|
|
|
355
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
356
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
357
|
+
}
|
|
358
|
+
|
|
87
359
|
function isFileNotFound(error: unknown): boolean {
|
|
88
360
|
return (
|
|
89
361
|
typeof error === "object" &&
|
|
@@ -5,8 +5,16 @@ export type SlashCommand = {
|
|
|
5
5
|
|
|
6
6
|
export const SLASH_COMMANDS: readonly SlashCommand[] = [
|
|
7
7
|
{ name: "status", description: "Show session and context details" },
|
|
8
|
-
{ name: "
|
|
8
|
+
{ name: "skills", description: "Show available and active Agent Skills" },
|
|
9
|
+
{ name: "mcp", description: "Show MCP servers and runtime tools" },
|
|
10
|
+
{
|
|
11
|
+
name: "compact",
|
|
12
|
+
description: "Swap tool output or retire a cold history prefix",
|
|
13
|
+
},
|
|
14
|
+
{ name: "clear", description: "Start a new session and clear conversation" },
|
|
15
|
+
{ name: "fork", description: "Clone the current session" },
|
|
9
16
|
{ name: "view", description: "View a local UTF-8 text file" },
|
|
17
|
+
{ name: "copy", description: "Copy the last response as Markdown" },
|
|
10
18
|
{ name: "model", description: "Switch model profile (new session)" },
|
|
11
19
|
{ name: "resume", description: "Choose or resume a session" },
|
|
12
20
|
{ name: "session", description: "Manage stored sessions" },
|
|
@@ -15,8 +23,14 @@ export const SLASH_COMMANDS: readonly SlashCommand[] = [
|
|
|
15
23
|
|
|
16
24
|
export type ParsedSlashCommand =
|
|
17
25
|
| { type: "status" }
|
|
26
|
+
| { type: "skills" }
|
|
27
|
+
| { type: "mcp" }
|
|
18
28
|
| { type: "compact" }
|
|
29
|
+
| { type: "compact_retire" }
|
|
30
|
+
| { type: "clear" }
|
|
31
|
+
| { type: "fork" }
|
|
19
32
|
| { type: "view"; filePath: string }
|
|
33
|
+
| { type: "copy" }
|
|
20
34
|
| { type: "quit" }
|
|
21
35
|
| { type: "model" }
|
|
22
36
|
| { type: "model_switch"; profileName: string }
|
|
@@ -49,11 +63,41 @@ export function parseSlashCommand(input: string): ParsedSlashCommand {
|
|
|
49
63
|
if (command === "/status" && tokens.length === 1) {
|
|
50
64
|
return { type: "status" };
|
|
51
65
|
}
|
|
66
|
+
if (command === "/skills" && tokens.length === 1) {
|
|
67
|
+
return { type: "skills" };
|
|
68
|
+
}
|
|
69
|
+
if (command === "/mcp") {
|
|
70
|
+
if (tokens.length === 1) {
|
|
71
|
+
return { type: "mcp" };
|
|
72
|
+
}
|
|
73
|
+
throw new SlashCommandError("Usage: /mcp");
|
|
74
|
+
}
|
|
52
75
|
if (command === "/compact") {
|
|
53
76
|
if (tokens.length === 1) {
|
|
54
77
|
return { type: "compact" };
|
|
55
78
|
}
|
|
56
|
-
|
|
79
|
+
if (tokens.length === 2 && tokens[1] === "retire") {
|
|
80
|
+
return { type: "compact_retire" };
|
|
81
|
+
}
|
|
82
|
+
throw new SlashCommandError("Usage: /compact [retire]");
|
|
83
|
+
}
|
|
84
|
+
if (command === "/clear") {
|
|
85
|
+
if (tokens.length === 1) {
|
|
86
|
+
return { type: "clear" };
|
|
87
|
+
}
|
|
88
|
+
throw new SlashCommandError("Usage: /clear");
|
|
89
|
+
}
|
|
90
|
+
if (command === "/fork") {
|
|
91
|
+
if (tokens.length === 1) {
|
|
92
|
+
return { type: "fork" };
|
|
93
|
+
}
|
|
94
|
+
throw new SlashCommandError("Usage: /fork");
|
|
95
|
+
}
|
|
96
|
+
if (command === "/copy") {
|
|
97
|
+
if (tokens.length === 1) {
|
|
98
|
+
return { type: "copy" };
|
|
99
|
+
}
|
|
100
|
+
throw new SlashCommandError("Usage: /copy");
|
|
57
101
|
}
|
|
58
102
|
if (command === "/quit" && tokens.length === 1) {
|
|
59
103
|
return { type: "quit" };
|
|
@@ -1,12 +1,21 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type {
|
|
2
|
+
ContextCompactionResult,
|
|
3
|
+
ContextRetirementResult,
|
|
4
|
+
} from "../context/context-manager";
|
|
2
5
|
import type {
|
|
3
6
|
ExecuteTurnInput,
|
|
7
|
+
AcceptedTurn,
|
|
4
8
|
RuntimeSession,
|
|
9
|
+
RuntimeSkillsSnapshot,
|
|
5
10
|
SessionDisposeReason,
|
|
6
11
|
} from "../agent/runtime-session";
|
|
7
|
-
import type { RunAgentResult } from "../agent/types";
|
|
12
|
+
import type { RunAgentResult, UserMessage } from "../agent/types";
|
|
13
|
+
import type { ImageAssetRef } from "../image/image-types";
|
|
14
|
+
import type { ImportedImageAsset } from "../image/image-asset-store";
|
|
8
15
|
import type { SessionId } from "../ids/runtime-id";
|
|
16
|
+
import { createUuidV7 } from "../ids/uuid-v7";
|
|
9
17
|
import type { ModelProfile } from "../cli/model-profiles";
|
|
18
|
+
import type { McpInventorySnapshot } from "../mcp/mcp-manager";
|
|
10
19
|
import { SessionCatalog, type SessionSummary } from "../session/session-catalog";
|
|
11
20
|
import type { TuiProjectionStore } from "./tui-projection-store";
|
|
12
21
|
|
|
@@ -16,7 +25,20 @@ export type TuiSessionBinding = {
|
|
|
16
25
|
workspaceRoot: string;
|
|
17
26
|
profileName?: string;
|
|
18
27
|
projectionStore: TuiProjectionStore;
|
|
19
|
-
|
|
28
|
+
skills(): RuntimeSkillsSnapshot;
|
|
29
|
+
mcp(): McpInventorySnapshot;
|
|
30
|
+
supportsImageInput?: () => boolean;
|
|
31
|
+
importImage?: (
|
|
32
|
+
sourcePath: string,
|
|
33
|
+
signal: AbortSignal,
|
|
34
|
+
prospectiveMessageImageCount: number,
|
|
35
|
+
) => Promise<ImportedImageAsset>;
|
|
36
|
+
verifyImageAssets?: (
|
|
37
|
+
assets: readonly ImageAssetRef[],
|
|
38
|
+
signal: AbortSignal,
|
|
39
|
+
) => Promise<void>;
|
|
40
|
+
admitTurn?: (userMessage: UserMessage, signal: AbortSignal) => Promise<AcceptedTurn>;
|
|
41
|
+
executeTurn(userMessage: UserMessage, signal: AbortSignal): Promise<RunAgentResult>;
|
|
20
42
|
};
|
|
21
43
|
|
|
22
44
|
export type TuiSessionController = {
|
|
@@ -24,6 +46,9 @@ export type TuiSessionController = {
|
|
|
24
46
|
subscribe: (listener: () => void) => () => void;
|
|
25
47
|
listSessions: () => Promise<readonly SessionSummary[]>;
|
|
26
48
|
compact: () => Promise<ContextCompactionResult>;
|
|
49
|
+
retire: () => Promise<ContextRetirementResult>;
|
|
50
|
+
fork: () => Promise<SessionId>;
|
|
51
|
+
clear: () => Promise<void>;
|
|
27
52
|
resume: (sessionId: SessionId) => Promise<void>;
|
|
28
53
|
delete: (sessionId: SessionId) => Promise<void>;
|
|
29
54
|
switchModel: (profile: ModelProfile) => Promise<void>;
|
|
@@ -47,6 +72,9 @@ export class DefaultTuiSessionController implements TuiSessionController {
|
|
|
47
72
|
private readonly createSessionWithProfile: (
|
|
48
73
|
profile: ModelProfile,
|
|
49
74
|
) => Promise<ManagedTuiSessionBinding>,
|
|
75
|
+
private readonly createFreshSession: (
|
|
76
|
+
current: TuiSessionBinding,
|
|
77
|
+
) => Promise<ManagedTuiSessionBinding>,
|
|
50
78
|
) {
|
|
51
79
|
this.binding = initial;
|
|
52
80
|
}
|
|
@@ -68,31 +96,42 @@ export class DefaultTuiSessionController implements TuiSessionController {
|
|
|
68
96
|
return this.serialize(() => this.binding.runtimeSession.compactContext());
|
|
69
97
|
}
|
|
70
98
|
|
|
99
|
+
retire(): Promise<ContextRetirementResult> {
|
|
100
|
+
return this.serialize(() => this.binding.runtimeSession.retireContext());
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
fork(): Promise<SessionId> {
|
|
104
|
+
return this.serialize(async () => {
|
|
105
|
+
const targetSessionId = createUuidV7() as SessionId;
|
|
106
|
+
await this.replaceSession(
|
|
107
|
+
"Cannot clone the session while a turn, context operation, or background task is active.",
|
|
108
|
+
async (current) => {
|
|
109
|
+
await current.runtimeSession.cloneSession(targetSessionId);
|
|
110
|
+
return this.openSession(targetSessionId);
|
|
111
|
+
},
|
|
112
|
+
);
|
|
113
|
+
return targetSessionId;
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
clear(): Promise<void> {
|
|
118
|
+
return this.serialize(() =>
|
|
119
|
+
this.replaceSession(
|
|
120
|
+
"Cannot clear the session while a turn, context operation, or background task is active.",
|
|
121
|
+
(current) => this.createFreshSession(current),
|
|
122
|
+
),
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
|
|
71
126
|
resume(sessionId: SessionId): Promise<void> {
|
|
72
127
|
return this.serialize(async () => {
|
|
73
|
-
|
|
74
|
-
if (sessionId === current.sessionId) {
|
|
128
|
+
if (sessionId === this.binding.sessionId) {
|
|
75
129
|
throw new Error(`Session ${sessionId} is already current.`);
|
|
76
130
|
}
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
const target = await this.openSession(sessionId);
|
|
84
|
-
try {
|
|
85
|
-
await current.runtimeSession.dispose({ type: "session_switch" });
|
|
86
|
-
} catch (error) {
|
|
87
|
-
await target.runtimeSession
|
|
88
|
-
.dispose({ type: "runner_failed", error: errorMessage(error) })
|
|
89
|
-
.catch(() => undefined);
|
|
90
|
-
throw error;
|
|
91
|
-
}
|
|
92
|
-
this.binding = target;
|
|
93
|
-
for (const listener of this.listeners) {
|
|
94
|
-
listener();
|
|
95
|
-
}
|
|
131
|
+
await this.replaceSession(
|
|
132
|
+
"Cannot switch sessions while a turn or background task is active.",
|
|
133
|
+
() => this.openSession(sessionId),
|
|
134
|
+
);
|
|
96
135
|
});
|
|
97
136
|
}
|
|
98
137
|
|
|
@@ -101,34 +140,44 @@ export class DefaultTuiSessionController implements TuiSessionController {
|
|
|
101
140
|
}
|
|
102
141
|
|
|
103
142
|
switchModel(profile: ModelProfile): Promise<void> {
|
|
104
|
-
return this.serialize(
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
const target = await this.createSessionWithProfile(profile);
|
|
113
|
-
try {
|
|
114
|
-
await current.runtimeSession.dispose({ type: "session_switch" });
|
|
115
|
-
} catch (error) {
|
|
116
|
-
await target.runtimeSession
|
|
117
|
-
.dispose({ type: "runner_failed", error: errorMessage(error) })
|
|
118
|
-
.catch(() => undefined);
|
|
119
|
-
throw error;
|
|
120
|
-
}
|
|
121
|
-
this.binding = target;
|
|
122
|
-
for (const listener of this.listeners) {
|
|
123
|
-
listener();
|
|
124
|
-
}
|
|
125
|
-
});
|
|
143
|
+
return this.serialize(() =>
|
|
144
|
+
this.replaceSession(
|
|
145
|
+
"Cannot switch models while a turn or background task is active.",
|
|
146
|
+
() => this.createSessionWithProfile(profile),
|
|
147
|
+
),
|
|
148
|
+
);
|
|
126
149
|
}
|
|
127
150
|
|
|
128
151
|
dispose(reason: SessionDisposeReason): Promise<void> {
|
|
129
152
|
return this.binding.runtimeSession.dispose(reason);
|
|
130
153
|
}
|
|
131
154
|
|
|
155
|
+
private async replaceSession(
|
|
156
|
+
unavailableMessage: string,
|
|
157
|
+
createTarget: (
|
|
158
|
+
current: ManagedTuiSessionBinding,
|
|
159
|
+
) => Promise<ManagedTuiSessionBinding>,
|
|
160
|
+
): Promise<void> {
|
|
161
|
+
const current = this.binding;
|
|
162
|
+
if (!current.runtimeSession.canSwitchSession()) {
|
|
163
|
+
throw new Error(unavailableMessage);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const target = await createTarget(current);
|
|
167
|
+
try {
|
|
168
|
+
await current.runtimeSession.dispose({ type: "session_switch" });
|
|
169
|
+
} catch (error) {
|
|
170
|
+
await target.runtimeSession
|
|
171
|
+
.dispose({ type: "runner_failed", error: errorMessage(error) })
|
|
172
|
+
.catch(() => undefined);
|
|
173
|
+
throw error;
|
|
174
|
+
}
|
|
175
|
+
this.binding = target;
|
|
176
|
+
for (const listener of this.listeners) {
|
|
177
|
+
listener();
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
132
181
|
private serialize<T>(operation: () => Promise<T>): Promise<T> {
|
|
133
182
|
if (this.operation !== undefined) {
|
|
134
183
|
return Promise.reject(new Error("Another session operation is already running."));
|
|
@@ -157,9 +206,22 @@ export function managedTuiBinding(input: {
|
|
|
157
206
|
profileName: input.profileName,
|
|
158
207
|
projectionStore: input.projectionStore,
|
|
159
208
|
runtimeSession: input.runtimeSession,
|
|
160
|
-
|
|
209
|
+
skills: () => input.runtimeSession.skills(),
|
|
210
|
+
mcp: () => input.runtimeSession.mcp(),
|
|
211
|
+
supportsImageInput: () => input.runtimeSession.supportsImageInput(),
|
|
212
|
+
importImage: (sourcePath, signal, prospectiveMessageImageCount) =>
|
|
213
|
+
input.runtimeSession.importImage(
|
|
214
|
+
sourcePath,
|
|
215
|
+
signal,
|
|
216
|
+
prospectiveMessageImageCount,
|
|
217
|
+
),
|
|
218
|
+
verifyImageAssets: (assets, signal) =>
|
|
219
|
+
input.runtimeSession.verifyImageAssets(assets, signal),
|
|
220
|
+
admitTurn: (userMessage, signal) =>
|
|
221
|
+
input.runtimeSession.admitTurn({ userMessage, signal }),
|
|
222
|
+
executeTurn: (userMessage, signal) =>
|
|
161
223
|
input.runtimeSession.executeTurn({
|
|
162
|
-
|
|
224
|
+
userMessage,
|
|
163
225
|
signal,
|
|
164
226
|
} satisfies ExecuteTurnInput),
|
|
165
227
|
};
|