pi-dsh-mimic 0.1.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/LICENSE +22 -0
- package/NOTICE +16 -0
- package/README.md +155 -0
- package/README.zh-CN.md +133 -0
- package/SECURITY.md +41 -0
- package/docs/advanced.md +182 -0
- package/docs/advanced.zh-CN.md +151 -0
- package/docs/project2-evidence.md +230 -0
- package/package.json +70 -0
- package/scripts/session-stats.mjs +117 -0
- package/src/constants.ts +33 -0
- package/src/editor.ts +133 -0
- package/src/index.ts +188 -0
- package/src/protocol.ts +134 -0
- package/src/session-stage.ts +67 -0
package/src/editor.ts
ADDED
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import {
|
|
2
|
+
existsSync,
|
|
3
|
+
mkdirSync,
|
|
4
|
+
readFileSync,
|
|
5
|
+
readdirSync,
|
|
6
|
+
statSync,
|
|
7
|
+
writeFileSync,
|
|
8
|
+
} from "node:fs";
|
|
9
|
+
import { dirname, isAbsolute, join, relative } from "node:path";
|
|
10
|
+
|
|
11
|
+
import { EDITOR_TRUNCATED_MESSAGE } from "./constants.js";
|
|
12
|
+
|
|
13
|
+
export type EditorCommand = "view" | "create" | "str_replace" | "insert";
|
|
14
|
+
|
|
15
|
+
export interface EditorArgs {
|
|
16
|
+
path: string;
|
|
17
|
+
command: EditorCommand;
|
|
18
|
+
view_range?: number[];
|
|
19
|
+
old_str?: string;
|
|
20
|
+
new_str?: string;
|
|
21
|
+
insert_line?: number;
|
|
22
|
+
file_text?: string;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const OUTPUT_LIMIT = 16_000;
|
|
26
|
+
|
|
27
|
+
function requireAbsolutePath(candidate: string): string {
|
|
28
|
+
if (candidate.trim() === "" || !isAbsolute(candidate)) {
|
|
29
|
+
throw new Error(`Path is not an absolute path: ${candidate}`);
|
|
30
|
+
}
|
|
31
|
+
return candidate;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function requireArgument(value: string | undefined, argument: string, command: EditorCommand): string {
|
|
35
|
+
if (value === undefined) throw new Error(`${command} requires ${argument}`);
|
|
36
|
+
return value;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function clip(value: string, limit: number): string {
|
|
40
|
+
if (value.length <= limit) return value;
|
|
41
|
+
return `${value.slice(0, limit)}\n${EDITOR_TRUNCATED_MESSAGE}`;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function directoryView(root: string): string {
|
|
45
|
+
const rows: string[] = [root];
|
|
46
|
+
const walk = (directory: string, depth: number): void => {
|
|
47
|
+
if (depth > 2) return;
|
|
48
|
+
const children = readdirSync(directory, { withFileTypes: true })
|
|
49
|
+
.filter((item) => !item.name.startsWith(".")
|
|
50
|
+
&& item.name !== "node_modules"
|
|
51
|
+
&& item.name !== "__pycache__")
|
|
52
|
+
.sort((a, b) => a.name.localeCompare(b.name));
|
|
53
|
+
for (const child of children) {
|
|
54
|
+
const absolute = join(directory, child.name);
|
|
55
|
+
rows.push(`${relative(root, absolute)}${child.isDirectory() ? "/" : ""}`);
|
|
56
|
+
if (child.isDirectory()) walk(absolute, depth + 1);
|
|
57
|
+
}
|
|
58
|
+
};
|
|
59
|
+
walk(root, 1);
|
|
60
|
+
return rows.join("\n");
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function fileView(path: string, range?: number[]): string {
|
|
64
|
+
const lines = readFileSync(path, "utf8").split("\n");
|
|
65
|
+
const start = range?.[0] ?? 1;
|
|
66
|
+
const requestedEnd = range?.[1] ?? -1;
|
|
67
|
+
const end = requestedEnd === -1 ? lines.length : requestedEnd;
|
|
68
|
+
if ((range !== undefined && range.length !== 2)
|
|
69
|
+
|| !Number.isInteger(start)
|
|
70
|
+
|| !Number.isInteger(end)
|
|
71
|
+
|| start < 1
|
|
72
|
+
|| end < start
|
|
73
|
+
|| end > lines.length) {
|
|
74
|
+
throw new Error(`Invalid view_range for ${path}`);
|
|
75
|
+
}
|
|
76
|
+
const body = lines.slice(start - 1, end)
|
|
77
|
+
.map((line, offset) => `${String(start + offset).padStart(6, " ")} ${line}`)
|
|
78
|
+
.join("\n");
|
|
79
|
+
return `Contents of ${path}:\n${body}`;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function replaceUnique(source: string, needle: string, replacement: string): string {
|
|
83
|
+
const match = source.indexOf(needle);
|
|
84
|
+
if (match === -1) throw new Error("old_str was not found");
|
|
85
|
+
if (source.indexOf(needle, match + needle.length) !== -1) {
|
|
86
|
+
throw new Error("Multiple matches found for old_str");
|
|
87
|
+
}
|
|
88
|
+
return source.slice(0, match) + replacement + source.slice(match + needle.length);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export function executeEditor(args: EditorArgs, outputLimit = OUTPUT_LIMIT): string {
|
|
92
|
+
const path = requireAbsolutePath(args.path);
|
|
93
|
+
|
|
94
|
+
if (args.command === "create") {
|
|
95
|
+
if (existsSync(path)) throw new Error(`Cannot create an existing path: ${path}`);
|
|
96
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
97
|
+
writeFileSync(path, requireArgument(args.file_text, "file_text", args.command), {
|
|
98
|
+
encoding: "utf8",
|
|
99
|
+
flag: "wx",
|
|
100
|
+
});
|
|
101
|
+
return `Created successfully: ${path}`;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
if (!existsSync(path)) throw new Error(`Path does not exist: ${path}`);
|
|
105
|
+
const info = statSync(path);
|
|
106
|
+
|
|
107
|
+
if (args.command === "view") {
|
|
108
|
+
const output = info.isDirectory() ? directoryView(path) : fileView(path, args.view_range);
|
|
109
|
+
return clip(output, outputLimit);
|
|
110
|
+
}
|
|
111
|
+
if (!info.isFile()) throw new Error(`Editing requires a regular file: ${path}`);
|
|
112
|
+
|
|
113
|
+
const source = readFileSync(path, "utf8");
|
|
114
|
+
if (args.command === "str_replace") {
|
|
115
|
+
const next = replaceUnique(
|
|
116
|
+
source,
|
|
117
|
+
requireArgument(args.old_str, "old_str", args.command),
|
|
118
|
+
args.new_str ?? "",
|
|
119
|
+
);
|
|
120
|
+
writeFileSync(path, next, "utf8");
|
|
121
|
+
return `Replaced text in ${path}`;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
if (!Number.isInteger(args.insert_line)) throw new Error("insert requires insert_line");
|
|
125
|
+
const insertAfter = args.insert_line as number;
|
|
126
|
+
const lines = source.split("\n");
|
|
127
|
+
if (insertAfter < 0 || insertAfter > lines.length) {
|
|
128
|
+
throw new Error(`insert_line is outside the file: ${insertAfter}`);
|
|
129
|
+
}
|
|
130
|
+
lines.splice(insertAfter, 0, requireArgument(args.new_str, "new_str", args.command));
|
|
131
|
+
writeFileSync(path, lines.join("\n"), "utf8");
|
|
132
|
+
return `Inserted text in ${path}`;
|
|
133
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
ExtensionAPI,
|
|
3
|
+
ExtensionContext,
|
|
4
|
+
} from "@earendil-works/pi-coding-agent";
|
|
5
|
+
import { StringEnum } from "@earendil-works/pi-ai";
|
|
6
|
+
import { Type } from "typebox";
|
|
7
|
+
|
|
8
|
+
import {
|
|
9
|
+
BOOTSTRAP_TOOL_NAMES,
|
|
10
|
+
EDITOR_DESCRIPTION,
|
|
11
|
+
MINIMAL_BASH_DESCRIPTION,
|
|
12
|
+
MINIMAL_PERSONA,
|
|
13
|
+
STATE_ENTRY_TYPE,
|
|
14
|
+
TARGET_MODEL_ID,
|
|
15
|
+
TARGET_PROVIDERS,
|
|
16
|
+
} from "./constants.js";
|
|
17
|
+
import { executeEditor, type EditorArgs } from "./editor.js";
|
|
18
|
+
import {
|
|
19
|
+
isJsonObject,
|
|
20
|
+
keepMinimalPersona,
|
|
21
|
+
makeBootstrapRequest,
|
|
22
|
+
} from "./protocol.js";
|
|
23
|
+
import {
|
|
24
|
+
conversationHasStarted,
|
|
25
|
+
type LiveStage,
|
|
26
|
+
restoreStage,
|
|
27
|
+
supportedRoute,
|
|
28
|
+
} from "./session-stage.js";
|
|
29
|
+
|
|
30
|
+
const STATUS_ID = "pi-dsh-mimic";
|
|
31
|
+
|
|
32
|
+
export default function piDshAnchor(api: ExtensionAPI): void {
|
|
33
|
+
const sessions = new Map<string, LiveStage>();
|
|
34
|
+
let editorAvailable = false;
|
|
35
|
+
|
|
36
|
+
const sessionKey = (context: ExtensionContext): string => context.sessionManager.getSessionId();
|
|
37
|
+
|
|
38
|
+
const exposeEditor = (): void => {
|
|
39
|
+
if (editorAvailable) return;
|
|
40
|
+
api.registerTool({
|
|
41
|
+
name: "str_replace_editor",
|
|
42
|
+
description: EDITOR_DESCRIPTION,
|
|
43
|
+
parameters: Type.Object({
|
|
44
|
+
command: StringEnum(["view", "create", "str_replace", "insert"] as const),
|
|
45
|
+
path: Type.String(),
|
|
46
|
+
file_text: Type.Optional(Type.String()),
|
|
47
|
+
insert_line: Type.Optional(Type.Integer()),
|
|
48
|
+
new_str: Type.Optional(Type.String()),
|
|
49
|
+
old_str: Type.Optional(Type.String()),
|
|
50
|
+
view_range: Type.Optional(Type.Array(Type.Integer())),
|
|
51
|
+
}),
|
|
52
|
+
label: "str_replace_editor",
|
|
53
|
+
async execute(_callId, input) {
|
|
54
|
+
const args = input as EditorArgs;
|
|
55
|
+
return {
|
|
56
|
+
content: [{ type: "text", text: executeEditor(args) }],
|
|
57
|
+
details: { command: args.command, path: args.path },
|
|
58
|
+
};
|
|
59
|
+
},
|
|
60
|
+
});
|
|
61
|
+
editorAvailable = true;
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
const showStage = (context: ExtensionContext, stage?: LiveStage): void => {
|
|
65
|
+
if (!context.hasUI) return;
|
|
66
|
+
context.ui.setStatus(
|
|
67
|
+
STATUS_ID,
|
|
68
|
+
stage?.stage === "bootstrap" ? "V4 one-shot: bash/editor" : undefined,
|
|
69
|
+
);
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
const saveStage = (stage: LiveStage): void => {
|
|
73
|
+
api.appendEntry(STATE_ENTRY_TYPE, {
|
|
74
|
+
schema: stage.schema,
|
|
75
|
+
route: stage.route,
|
|
76
|
+
stage: stage.stage,
|
|
77
|
+
});
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
const locateStage = (context: ExtensionContext, create: boolean): LiveStage | undefined => {
|
|
81
|
+
const route = supportedRoute(context.model);
|
|
82
|
+
if (route === undefined) return undefined;
|
|
83
|
+
|
|
84
|
+
const key = sessionKey(context);
|
|
85
|
+
const cached = sessions.get(key);
|
|
86
|
+
if (cached?.route === route) return cached;
|
|
87
|
+
|
|
88
|
+
const branch = context.sessionManager.getBranch();
|
|
89
|
+
const restored = restoreStage(branch, route);
|
|
90
|
+
if (restored !== undefined) {
|
|
91
|
+
sessions.set(key, restored);
|
|
92
|
+
return restored;
|
|
93
|
+
}
|
|
94
|
+
if (!create || conversationHasStarted(branch)) return undefined;
|
|
95
|
+
|
|
96
|
+
const stage: LiveStage = {
|
|
97
|
+
schema: 1,
|
|
98
|
+
route,
|
|
99
|
+
stage: "bootstrap",
|
|
100
|
+
requestIssued: false,
|
|
101
|
+
};
|
|
102
|
+
sessions.set(key, stage);
|
|
103
|
+
saveStage(stage);
|
|
104
|
+
return stage;
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
const enterExecution = (context: ExtensionContext, stage: LiveStage): void => {
|
|
108
|
+
if (stage.stage === "execute") return;
|
|
109
|
+
stage.stage = "execute";
|
|
110
|
+
stage.requestIssued = false;
|
|
111
|
+
saveStage(stage);
|
|
112
|
+
showStage(context, stage);
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
const reloadSession = (context: ExtensionContext): void => {
|
|
116
|
+
sessions.delete(sessionKey(context));
|
|
117
|
+
const stage = locateStage(context, false);
|
|
118
|
+
if (stage !== undefined) exposeEditor();
|
|
119
|
+
showStage(context, stage);
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
api.on("session_start", (_event, context) => reloadSession(context));
|
|
123
|
+
api.on("session_tree", (_event, context) => reloadSession(context));
|
|
124
|
+
api.on("session_shutdown", (_event, context) => {
|
|
125
|
+
sessions.delete(sessionKey(context));
|
|
126
|
+
showStage(context);
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
api.on("model_select", (_event, context) => {
|
|
130
|
+
const stage = locateStage(context, false);
|
|
131
|
+
if (stage !== undefined) exposeEditor();
|
|
132
|
+
showStage(context, stage);
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
api.on("input", (event, context) => {
|
|
136
|
+
if (event.source === "extension" || supportedRoute(context.model) === undefined) return;
|
|
137
|
+
const stage = locateStage(context, true);
|
|
138
|
+
if (stage !== undefined) exposeEditor();
|
|
139
|
+
showStage(context, stage);
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
api.on("before_agent_start", (event, context) => {
|
|
143
|
+
const stage = locateStage(context, false);
|
|
144
|
+
if (stage === undefined) {
|
|
145
|
+
showStage(context);
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
exposeEditor();
|
|
149
|
+
showStage(context, stage);
|
|
150
|
+
if (event.systemPrompt !== MINIMAL_PERSONA) return { systemPrompt: MINIMAL_PERSONA };
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
api.on("before_provider_request", (event, context) => {
|
|
154
|
+
const stage = locateStage(context, false);
|
|
155
|
+
if (stage === undefined || !isJsonObject(event.payload)) return;
|
|
156
|
+
exposeEditor();
|
|
157
|
+
|
|
158
|
+
if (stage.stage === "bootstrap") {
|
|
159
|
+
stage.requestIssued = true;
|
|
160
|
+
return makeBootstrapRequest(event.payload);
|
|
161
|
+
}
|
|
162
|
+
return keepMinimalPersona(event.payload);
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
api.on("message_end", (event, context) => {
|
|
166
|
+
const stage = locateStage(context, false);
|
|
167
|
+
if (stage?.stage !== "bootstrap" || !stage.requestIssued) return;
|
|
168
|
+
if (event.message.role !== "assistant"
|
|
169
|
+
|| event.message.stopReason === "error"
|
|
170
|
+
|| event.message.stopReason === "aborted") return;
|
|
171
|
+
enterExecution(context, stage);
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
api.on("tool_call", (_event, context) => {
|
|
175
|
+
const stage = locateStage(context, false);
|
|
176
|
+
if (stage?.stage === "bootstrap" && stage.requestIssued) enterExecution(context, stage);
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
export {
|
|
181
|
+
BOOTSTRAP_TOOL_NAMES,
|
|
182
|
+
EDITOR_DESCRIPTION,
|
|
183
|
+
MINIMAL_BASH_DESCRIPTION,
|
|
184
|
+
MINIMAL_PERSONA,
|
|
185
|
+
STATE_ENTRY_TYPE,
|
|
186
|
+
TARGET_MODEL_ID,
|
|
187
|
+
TARGET_PROVIDERS,
|
|
188
|
+
};
|
package/src/protocol.ts
ADDED
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import {
|
|
2
|
+
BOOTSTRAP_TOOL_NAMES,
|
|
3
|
+
EDITOR_DESCRIPTION,
|
|
4
|
+
MINIMAL_BASH_DESCRIPTION,
|
|
5
|
+
MINIMAL_PERSONA,
|
|
6
|
+
} from "./constants.js";
|
|
7
|
+
|
|
8
|
+
export type JsonObject = Record<string, unknown>;
|
|
9
|
+
|
|
10
|
+
export function isJsonObject(value: unknown): value is JsonObject {
|
|
11
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function encodeMinimalMessage(value: unknown): unknown {
|
|
15
|
+
if (!isJsonObject(value)) return value;
|
|
16
|
+
if (value.role === "system" || value.role === "developer") {
|
|
17
|
+
return { ...value, content: MINIMAL_PERSONA };
|
|
18
|
+
}
|
|
19
|
+
if (!Array.isArray(value.content)) return value;
|
|
20
|
+
|
|
21
|
+
const textOnly = value.content.every((part) => isJsonObject(part)
|
|
22
|
+
&& part.type === "text"
|
|
23
|
+
&& typeof part.text === "string");
|
|
24
|
+
if (!textOnly) return value;
|
|
25
|
+
return {
|
|
26
|
+
...value,
|
|
27
|
+
content: value.content.map((part) => String((part as JsonObject).text)).join(""),
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function minimalToolSchemas(): unknown[] {
|
|
32
|
+
const bash = {
|
|
33
|
+
type: "function",
|
|
34
|
+
function: {
|
|
35
|
+
name: BOOTSTRAP_TOOL_NAMES[0],
|
|
36
|
+
description: MINIMAL_BASH_DESCRIPTION,
|
|
37
|
+
parameters: {
|
|
38
|
+
type: "object",
|
|
39
|
+
properties: {
|
|
40
|
+
command: {
|
|
41
|
+
type: "string",
|
|
42
|
+
description: "The bash command to run. Relative path is preferred in the command.",
|
|
43
|
+
},
|
|
44
|
+
},
|
|
45
|
+
required: ["command"],
|
|
46
|
+
},
|
|
47
|
+
strict: false,
|
|
48
|
+
},
|
|
49
|
+
};
|
|
50
|
+
const editor = {
|
|
51
|
+
type: "function",
|
|
52
|
+
function: {
|
|
53
|
+
name: BOOTSTRAP_TOOL_NAMES[1],
|
|
54
|
+
description: EDITOR_DESCRIPTION,
|
|
55
|
+
parameters: {
|
|
56
|
+
type: "object",
|
|
57
|
+
properties: {
|
|
58
|
+
command: {
|
|
59
|
+
type: "string",
|
|
60
|
+
description: "The commands to run. Allowed options are: `view`, `create`, `str_replace`, `insert`.",
|
|
61
|
+
enum: ["view", "create", "str_replace", "insert"],
|
|
62
|
+
},
|
|
63
|
+
path: {
|
|
64
|
+
type: "string",
|
|
65
|
+
description: "Absolute path to file or directory, e.g. `/repo/file.py` or `/repo`.",
|
|
66
|
+
},
|
|
67
|
+
file_text: {
|
|
68
|
+
type: "string",
|
|
69
|
+
description: "Required parameter of `create` command, with the content of the file to be created.",
|
|
70
|
+
},
|
|
71
|
+
insert_line: {
|
|
72
|
+
type: "integer",
|
|
73
|
+
description: "Required parameter of `insert` command. The `new_str` will be inserted AFTER the line `insert_line` of `path`.",
|
|
74
|
+
},
|
|
75
|
+
new_str: {
|
|
76
|
+
type: "string",
|
|
77
|
+
description: "Optional parameter of `str_replace` command containing the new string (if not given, no string will be added). Required parameter of `insert` command containing the string to insert.",
|
|
78
|
+
},
|
|
79
|
+
old_str: {
|
|
80
|
+
type: "string",
|
|
81
|
+
description: "Required parameter of `str_replace` command containing the string in `path` to replace.",
|
|
82
|
+
},
|
|
83
|
+
view_range: {
|
|
84
|
+
type: "array",
|
|
85
|
+
description: "Optional parameter of `view` command when `path` points to a file. If none is given, the full file is shown. If provided, the file will be shown in the indicated line number range, e.g. [11, 12] will show lines 11 and 12. Indexing at 1 to start. Setting `[start_line, -1]` shows all lines from `start_line` to the end of the file.",
|
|
86
|
+
items: { type: "integer" },
|
|
87
|
+
},
|
|
88
|
+
},
|
|
89
|
+
required: ["command", "path"],
|
|
90
|
+
},
|
|
91
|
+
strict: false,
|
|
92
|
+
},
|
|
93
|
+
};
|
|
94
|
+
return [bash, editor];
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export function makeBootstrapRequest(source: JsonObject): JsonObject {
|
|
98
|
+
const messages = Array.isArray(source.messages)
|
|
99
|
+
? source.messages.map(encodeMinimalMessage)
|
|
100
|
+
: [];
|
|
101
|
+
const request: JsonObject = {};
|
|
102
|
+
const values: JsonObject = {
|
|
103
|
+
...source,
|
|
104
|
+
messages,
|
|
105
|
+
tools: minimalToolSchemas(),
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
for (const field of [
|
|
109
|
+
"model",
|
|
110
|
+
"messages",
|
|
111
|
+
"stream",
|
|
112
|
+
"stream_options",
|
|
113
|
+
"thinking",
|
|
114
|
+
"reasoning_effort",
|
|
115
|
+
"tools",
|
|
116
|
+
"max_tokens",
|
|
117
|
+
]) {
|
|
118
|
+
if (Object.prototype.hasOwnProperty.call(values, field)) request[field] = values[field];
|
|
119
|
+
}
|
|
120
|
+
return request;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export function keepMinimalPersona(source: JsonObject): JsonObject | undefined {
|
|
124
|
+
if (!Array.isArray(source.messages)) return undefined;
|
|
125
|
+
let changed = false;
|
|
126
|
+
const messages = source.messages.map((message) => {
|
|
127
|
+
if (!isJsonObject(message)
|
|
128
|
+
|| (message.role !== "system" && message.role !== "developer")
|
|
129
|
+
|| message.content === MINIMAL_PERSONA) return message;
|
|
130
|
+
changed = true;
|
|
131
|
+
return { ...message, content: MINIMAL_PERSONA };
|
|
132
|
+
});
|
|
133
|
+
return changed ? { ...source, messages } : undefined;
|
|
134
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import {
|
|
2
|
+
STATE_ENTRY_TYPE,
|
|
3
|
+
TARGET_MODEL_ID,
|
|
4
|
+
TARGET_PROVIDERS,
|
|
5
|
+
} from "./constants.js";
|
|
6
|
+
import { isJsonObject } from "./protocol.js";
|
|
7
|
+
|
|
8
|
+
export type Stage = "bootstrap" | "execute";
|
|
9
|
+
|
|
10
|
+
export interface StageRecord {
|
|
11
|
+
schema: 1;
|
|
12
|
+
route: string;
|
|
13
|
+
stage: Stage;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface LiveStage extends StageRecord {
|
|
17
|
+
requestIssued: boolean;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function supportedRoute(model: { provider?: unknown; id?: unknown } | undefined): string | undefined {
|
|
21
|
+
if (typeof model?.provider !== "string" || typeof model.id !== "string") return undefined;
|
|
22
|
+
const provider = model.provider.toLowerCase();
|
|
23
|
+
const modelId = model.id.toLowerCase();
|
|
24
|
+
if (modelId !== TARGET_MODEL_ID || !TARGET_PROVIDERS.some((candidate) => candidate === provider)) {
|
|
25
|
+
return undefined;
|
|
26
|
+
}
|
|
27
|
+
return `${provider}/${modelId}`;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function conversationHasStarted(entries: readonly unknown[]): boolean {
|
|
31
|
+
return entries.some((entry) => isJsonObject(entry)
|
|
32
|
+
&& entry.type === "message"
|
|
33
|
+
&& isJsonObject(entry.message)
|
|
34
|
+
&& ["user", "assistant", "toolResult"].includes(String(entry.message.role)));
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function completedMessage(entry: unknown): boolean {
|
|
38
|
+
if (!isJsonObject(entry) || entry.type !== "message" || !isJsonObject(entry.message)) return false;
|
|
39
|
+
if (entry.message.role === "toolResult") return true;
|
|
40
|
+
return entry.message.role === "assistant"
|
|
41
|
+
&& entry.message.stopReason !== "error"
|
|
42
|
+
&& entry.message.stopReason !== "aborted";
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function decodeRecord(value: unknown, route: string): StageRecord | undefined {
|
|
46
|
+
if (!isJsonObject(value)
|
|
47
|
+
|| value.schema !== 1
|
|
48
|
+
|| value.route !== route
|
|
49
|
+
|| (value.stage !== "bootstrap" && value.stage !== "execute")) return undefined;
|
|
50
|
+
return { schema: 1, route, stage: value.stage };
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function restoreStage(entries: readonly unknown[], route: string): LiveStage | undefined {
|
|
54
|
+
for (let index = entries.length - 1; index >= 0; index -= 1) {
|
|
55
|
+
const entry = entries[index];
|
|
56
|
+
if (!isJsonObject(entry)
|
|
57
|
+
|| entry.type !== "custom"
|
|
58
|
+
|| entry.customType !== STATE_ENTRY_TYPE) continue;
|
|
59
|
+
const record = decodeRecord(entry.data, route);
|
|
60
|
+
if (record === undefined) continue;
|
|
61
|
+
const stage = record.stage === "bootstrap" && entries.slice(index + 1).some(completedMessage)
|
|
62
|
+
? "execute"
|
|
63
|
+
: record.stage;
|
|
64
|
+
return { ...record, stage, requestIssued: false };
|
|
65
|
+
}
|
|
66
|
+
return undefined;
|
|
67
|
+
}
|