@code-yeongyu/senpi 2026.9.25 → 2026.9.27-2
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 +58 -0
- package/dist/bun-runtime.d.ts +18 -2
- package/dist/bun-runtime.d.ts.map +1 -1
- package/dist/bun-runtime.js +60 -8
- package/dist/bun-runtime.js.map +1 -1
- package/dist/bundle/chunks/{app-server-command-RB6QUPRI.js → app-server-command-ATWHNOOO.js} +1 -1
- package/dist/bundle/chunks/bun-extension-importer-R4HXF3GD.js +14 -0
- package/dist/bundle/chunks/chunk-2L26KPZ6.js +8 -0
- package/dist/bundle/chunks/{chunk-3WBSBDBX.js → chunk-3CC5ZOBV.js} +1 -1
- package/dist/bundle/chunks/{chunk-OZCB6BP3.js → chunk-67BC7VZV.js} +4 -4
- package/dist/bundle/chunks/{chunk-JJ3NJZBD.js → chunk-6SLQ5HFO.js} +48 -45
- package/dist/bundle/chunks/{chunk-MKAM7IRH.js → chunk-6W2DB2GC.js} +1 -1
- package/dist/bundle/chunks/{chunk-BSOJHK4W.js → chunk-7FXXRPB3.js} +2 -1
- package/dist/bundle/chunks/{chunk-Y54V65MS.js → chunk-HPY47AVX.js} +1 -1
- package/dist/bundle/chunks/{chunk-CLZVNQW3.js → chunk-LPFZGWG2.js} +1 -1
- package/dist/bundle/chunks/chunk-W3D3ZYDS.js +2 -0
- package/dist/bundle/chunks/{chunk-UBPMXBOJ.js → chunk-XKLR7C5B.js} +1 -1
- package/dist/bundle/chunks/{cli-main-KO53KYDN.js → cli-main-HOM6UH3H.js} +1 -1
- package/dist/bundle/chunks/{help-fast-path-ZGKPSDFO.js → help-fast-path-YGYHMV2W.js} +1 -1
- package/dist/bundle/chunks/{host-command-BMHJ4YUD.js → host-command-XXSAFWBW.js} +1 -1
- package/dist/bundle/chunks/{host-lifecycle-2GZAGSGW.js → host-lifecycle-GOMGJ25O.js} +1 -1
- package/dist/bundle/chunks/{interactive-host-runtime-YI75GFD5.js → interactive-host-runtime-IYCZWJGX.js} +1 -1
- package/dist/bundle/chunks/{interactive-mode-ABDNLJSI.js → interactive-mode-ZMO7YVYU.js} +1 -1
- package/dist/bundle/chunks/{multi-session-host-JV3MNWXZ.js → multi-session-host-CE3W6COI.js} +1 -1
- package/dist/bundle/chunks/{package-manager-cli-ZYYAVCXU.js → package-manager-cli-2PHRL2SX.js} +1 -1
- package/dist/bundle/chunks/{rpc-mode-Q2OUJURB.js → rpc-mode-WCETTMFV.js} +1 -1
- package/dist/bundle/chunks/session-worker.js +135 -131
- package/dist/bundle/cli.js +1 -1
- package/dist/bundle/index.js +1 -1
- package/dist/bundle/rpc-entry.js +1 -1
- package/dist/cli/args.d.ts +1 -0
- package/dist/cli/args.d.ts.map +1 -1
- package/dist/cli/args.js +4 -0
- package/dist/cli/args.js.map +1 -1
- package/dist/cli/cross-project-session.d.ts +22 -0
- package/dist/cli/cross-project-session.d.ts.map +1 -0
- package/dist/cli/cross-project-session.js +38 -0
- package/dist/cli/cross-project-session.js.map +1 -0
- package/dist/core/agent-session.d.ts.map +1 -1
- package/dist/core/agent-session.js +19 -5
- package/dist/core/agent-session.js.map +1 -1
- package/dist/core/dynamic-prompt/handoff.js +1 -1
- package/dist/core/dynamic-prompt/handoff.js.map +1 -1
- package/dist/core/extensions/builtin/index.d.ts.map +1 -1
- package/dist/core/extensions/builtin/index.js +2 -0
- package/dist/core/extensions/builtin/index.js.map +1 -1
- package/dist/core/extensions/builtin/permission-system/index.d.ts.map +1 -1
- package/dist/core/extensions/builtin/permission-system/index.js +5 -2
- package/dist/core/extensions/builtin/permission-system/index.js.map +1 -1
- package/dist/core/extensions/builtin/permission-system/parsers.d.ts +5 -0
- package/dist/core/extensions/builtin/permission-system/parsers.d.ts.map +1 -1
- package/dist/core/extensions/builtin/permission-system/parsers.js +13 -0
- package/dist/core/extensions/builtin/permission-system/parsers.js.map +1 -1
- package/dist/core/extensions/builtin/repository-identity.d.ts +6 -0
- package/dist/core/extensions/builtin/repository-identity.d.ts.map +1 -0
- package/dist/core/extensions/builtin/repository-identity.js +33 -0
- package/dist/core/extensions/builtin/repository-identity.js.map +1 -0
- package/dist/core/extensions/builtin/todotools/state.d.ts +1 -1
- package/dist/core/extensions/builtin/todotools/state.d.ts.map +1 -1
- package/dist/core/extensions/builtin/todotools/state.js +1 -1
- package/dist/core/extensions/builtin/todotools/state.js.map +1 -1
- package/dist/core/extensions/builtin/ttsr/detectors/repetitive-turns.d.ts.map +1 -1
- package/dist/core/extensions/builtin/ttsr/detectors/repetitive-turns.js +3 -1
- package/dist/core/extensions/builtin/ttsr/detectors/repetitive-turns.js.map +1 -1
- package/dist/core/extensions/bun-extension-importer.d.ts.map +1 -1
- package/dist/core/extensions/bun-extension-importer.js +10 -4
- package/dist/core/extensions/bun-extension-importer.js.map +1 -1
- package/dist/core/extensions/bun-extension-registry.js +6 -5
- package/dist/core/extensions/bun-extension-registry.js.map +1 -1
- package/dist/core/extensions/index.d.ts +1 -1
- package/dist/core/extensions/index.d.ts.map +1 -1
- package/dist/core/extensions/index.js.map +1 -1
- package/dist/core/extensions/kernel-prelude.d.ts +13 -0
- package/dist/core/extensions/kernel-prelude.d.ts.map +1 -0
- package/dist/core/extensions/kernel-prelude.js +38 -0
- package/dist/core/extensions/kernel-prelude.js.map +1 -0
- package/dist/core/extensions/types.d.ts +44 -2
- package/dist/core/extensions/types.d.ts.map +1 -1
- package/dist/core/extensions/types.js.map +1 -1
- package/dist/core/messages.d.ts.map +1 -1
- package/dist/core/messages.js +4 -2
- package/dist/core/messages.js.map +1 -1
- package/dist/core/model-resolver.js +1 -1
- package/dist/core/model-resolver.js.map +1 -1
- package/dist/core/provider-rejected-images.d.ts +18 -0
- package/dist/core/provider-rejected-images.d.ts.map +1 -0
- package/dist/core/provider-rejected-images.js +107 -0
- package/dist/core/provider-rejected-images.js.map +1 -0
- package/dist/core/repository-identity.d.ts +18 -0
- package/dist/core/repository-identity.d.ts.map +1 -0
- package/dist/core/repository-identity.js +75 -0
- package/dist/core/repository-identity.js.map +1 -0
- package/dist/core/session-rebind.d.ts +17 -0
- package/dist/core/session-rebind.d.ts.map +1 -0
- package/dist/core/session-rebind.js +111 -0
- package/dist/core/session-rebind.js.map +1 -0
- package/dist/core/session-title-generator.d.ts.map +1 -1
- package/dist/core/session-title-generator.js +21 -3
- package/dist/core/session-title-generator.js.map +1 -1
- package/dist/core/session-title-reasoning.d.ts +14 -0
- package/dist/core/session-title-reasoning.d.ts.map +1 -0
- package/dist/core/session-title-reasoning.js +31 -0
- package/dist/core/session-title-reasoning.js.map +1 -0
- package/dist/core/tools/path-utils.d.ts.map +1 -1
- package/dist/core/tools/path-utils.js +13 -0
- package/dist/core/tools/path-utils.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js.map +1 -1
- package/dist/main.d.ts +6 -0
- package/dist/main.d.ts.map +1 -1
- package/dist/main.js +100 -19
- package/dist/main.js.map +1 -1
- package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
- package/dist/modes/interactive/interactive-mode.js +2 -0
- package/dist/modes/interactive/interactive-mode.js.map +1 -1
- package/dist/modes/interactive/runtime-notice-presenter.d.ts +8 -0
- package/dist/modes/interactive/runtime-notice-presenter.d.ts.map +1 -0
- package/dist/modes/interactive/runtime-notice-presenter.js +69 -0
- package/dist/modes/interactive/runtime-notice-presenter.js.map +1 -0
- package/dist/modes/interactive/runtime-notice.d.ts +50 -0
- package/dist/modes/interactive/runtime-notice.d.ts.map +1 -0
- package/dist/modes/interactive/runtime-notice.js +104 -0
- package/dist/modes/interactive/runtime-notice.js.map +1 -0
- package/docs/environment-variables.md +2 -0
- package/docs/extensions.md +27 -1
- package/docs/quickstart.md +7 -1
- package/docs/sessions.md +9 -0
- package/docs/usage.md +1 -0
- package/node_modules/@code-yeongyu/senpi-codemode/CHANGELOG.md +38 -0
- package/node_modules/@code-yeongyu/senpi-codemode/README.md +7 -0
- package/node_modules/@code-yeongyu/senpi-codemode/package.json +4 -4
- package/node_modules/@code-yeongyu/senpi-codemode/src/bridge/protocol.ts +9 -0
- package/node_modules/@code-yeongyu/senpi-codemode/src/bridges/schema-bridge.ts +2 -0
- package/node_modules/@code-yeongyu/senpi-codemode/src/extension/kernel-preludes.ts +30 -0
- package/node_modules/@code-yeongyu/senpi-codemode/src/index.ts +15 -1
- package/node_modules/@code-yeongyu/senpi-codemode/src/kernels/js/context-manager.ts +1 -1
- package/node_modules/@code-yeongyu/senpi-codemode/src/kernels/js/local-module-loader.ts +16 -2
- package/node_modules/@code-yeongyu/senpi-codemode/src/kernels/py/kernel.ts +6 -1
- package/node_modules/@code-yeongyu/senpi-codemode/src/kernels/py/prelude.py +15 -2
- package/node_modules/@code-yeongyu/senpi-codemode/src/kernels/py/transport.ts +7 -1
- package/node_modules/@code-yeongyu/senpi-codemode/src/kernels/shared/kernel-prelude-plan.ts +19 -0
- package/node_modules/@code-yeongyu/senpi-codemode/src/prompt/eval-prompt-template.ts +2 -1
- package/node_modules/@code-yeongyu/senpi-codemode/src/prompt/eval-prompt.ts +4 -0
- package/node_modules/@code-yeongyu/senpi-codemode/src/tool/eval-tool-options.ts +11 -1
- package/node_modules/@code-yeongyu/senpi-codemode/src/tool/eval-tool.ts +1 -0
- package/node_modules/@code-yeongyu/senpi-codemode/src/tool/run-eval-cell.ts +1 -0
- package/node_modules/@code-yeongyu/senpi-codemode/src/tool/types.ts +3 -1
- package/node_modules/@earendil-works/pi-agent-core/package.json +3 -3
- package/node_modules/@earendil-works/pi-ai/dist/providers/data/.manifest.json +1 -1
- package/node_modules/@earendil-works/pi-ai/dist/providers/data/fireworks.json +1 -1
- package/node_modules/@earendil-works/pi-ai/dist/providers/data/opencode-go.json +1 -1
- package/node_modules/@earendil-works/pi-ai/dist/providers/data/opencode.json +1 -1
- package/node_modules/@earendil-works/pi-ai/dist/providers/data/openrouter.json +1 -1
- package/node_modules/@earendil-works/pi-ai/dist/providers/data/vercel-ai-gateway.json +1 -1
- package/node_modules/@earendil-works/pi-ai/package.json +2 -2
- package/node_modules/@earendil-works/pi-pty/native/prebuilds/darwin-arm64/senpi_pty.darwin-arm64.node +0 -0
- package/node_modules/@earendil-works/pi-pty/package.json +1 -1
- package/node_modules/@earendil-works/pi-telemetry/package.json +1 -1
- package/node_modules/@earendil-works/pi-tui/package.json +1 -1
- package/package.json +7 -22
- package/dist/bundle/chunks/bun-extension-importer-H4IJCWWQ.js +0 -14
- package/dist/bundle/chunks/chunk-34HMVB33.js +0 -2
- package/dist/bundle/chunks/chunk-SGYEOIOJ.js +0 -8
- package/node_modules/@code-yeongyu/senpi-desktop-engine/README.md +0 -31
- package/node_modules/@code-yeongyu/senpi-desktop-engine/dist/handshake.d.ts +0 -38
- package/node_modules/@code-yeongyu/senpi-desktop-engine/dist/handshake.d.ts.map +0 -1
- package/node_modules/@code-yeongyu/senpi-desktop-engine/dist/handshake.js +0 -117
- package/node_modules/@code-yeongyu/senpi-desktop-engine/dist/handshake.js.map +0 -1
- package/node_modules/@code-yeongyu/senpi-desktop-engine/dist/index.d.ts +0 -3
- package/node_modules/@code-yeongyu/senpi-desktop-engine/dist/index.d.ts.map +0 -1
- package/node_modules/@code-yeongyu/senpi-desktop-engine/dist/index.js +0 -3
- package/node_modules/@code-yeongyu/senpi-desktop-engine/dist/index.js.map +0 -1
- package/node_modules/@code-yeongyu/senpi-desktop-engine/dist/locator.d.ts +0 -42
- package/node_modules/@code-yeongyu/senpi-desktop-engine/dist/locator.d.ts.map +0 -1
- package/node_modules/@code-yeongyu/senpi-desktop-engine/dist/locator.js +0 -79
- package/node_modules/@code-yeongyu/senpi-desktop-engine/dist/locator.js.map +0 -1
- package/node_modules/@code-yeongyu/senpi-desktop-engine/native/check-prebuild-fresh.mjs +0 -160
- package/node_modules/@code-yeongyu/senpi-desktop-engine/native/index.d.ts +0 -20
- package/node_modules/@code-yeongyu/senpi-desktop-engine/native/index.js +0 -23
- package/node_modules/@code-yeongyu/senpi-desktop-engine/native/prebuilds/darwin-arm64/senpi-desktop-engine +0 -0
- package/node_modules/@code-yeongyu/senpi-desktop-engine/package.json +0 -53
- package/node_modules/@code-yeongyu/senpi-desktop-prelude/README.md +0 -20
- package/node_modules/@code-yeongyu/senpi-desktop-prelude/dist/index.d.ts +0 -2
- package/node_modules/@code-yeongyu/senpi-desktop-prelude/dist/index.d.ts.map +0 -1
- package/node_modules/@code-yeongyu/senpi-desktop-prelude/dist/index.js +0 -2
- package/node_modules/@code-yeongyu/senpi-desktop-prelude/dist/index.js.map +0 -1
- package/node_modules/@code-yeongyu/senpi-desktop-prelude/package.json +0 -44
- package/node_modules/@code-yeongyu/senpi-desktop-protocol/README.md +0 -20
- package/node_modules/@code-yeongyu/senpi-desktop-protocol/dist/audit.d.ts +0 -38
- package/node_modules/@code-yeongyu/senpi-desktop-protocol/dist/audit.d.ts.map +0 -1
- package/node_modules/@code-yeongyu/senpi-desktop-protocol/dist/audit.js +0 -2
- package/node_modules/@code-yeongyu/senpi-desktop-protocol/dist/audit.js.map +0 -1
- package/node_modules/@code-yeongyu/senpi-desktop-protocol/dist/call.d.ts +0 -28
- package/node_modules/@code-yeongyu/senpi-desktop-protocol/dist/call.d.ts.map +0 -1
- package/node_modules/@code-yeongyu/senpi-desktop-protocol/dist/call.js +0 -102
- package/node_modules/@code-yeongyu/senpi-desktop-protocol/dist/call.js.map +0 -1
- package/node_modules/@code-yeongyu/senpi-desktop-protocol/dist/engine-schema.generated.d.ts +0 -14
- package/node_modules/@code-yeongyu/senpi-desktop-protocol/dist/engine-schema.generated.d.ts.map +0 -1
- package/node_modules/@code-yeongyu/senpi-desktop-protocol/dist/engine-schema.generated.js +0 -71
- package/node_modules/@code-yeongyu/senpi-desktop-protocol/dist/engine-schema.generated.js.map +0 -1
- package/node_modules/@code-yeongyu/senpi-desktop-protocol/dist/index.d.ts +0 -7
- package/node_modules/@code-yeongyu/senpi-desktop-protocol/dist/index.d.ts.map +0 -1
- package/node_modules/@code-yeongyu/senpi-desktop-protocol/dist/index.js +0 -4
- package/node_modules/@code-yeongyu/senpi-desktop-protocol/dist/index.js.map +0 -1
- package/node_modules/@code-yeongyu/senpi-desktop-protocol/dist/json-rpc.d.ts +0 -66
- package/node_modules/@code-yeongyu/senpi-desktop-protocol/dist/json-rpc.d.ts.map +0 -1
- package/node_modules/@code-yeongyu/senpi-desktop-protocol/dist/json-rpc.js +0 -8
- package/node_modules/@code-yeongyu/senpi-desktop-protocol/dist/json-rpc.js.map +0 -1
- package/node_modules/@code-yeongyu/senpi-desktop-protocol/dist/session.d.ts +0 -44
- package/node_modules/@code-yeongyu/senpi-desktop-protocol/dist/session.d.ts.map +0 -1
- package/node_modules/@code-yeongyu/senpi-desktop-protocol/dist/session.js +0 -2
- package/node_modules/@code-yeongyu/senpi-desktop-protocol/dist/session.js.map +0 -1
- package/node_modules/@code-yeongyu/senpi-desktop-protocol/dist/wire.d.ts +0 -59
- package/node_modules/@code-yeongyu/senpi-desktop-protocol/dist/wire.d.ts.map +0 -1
- package/node_modules/@code-yeongyu/senpi-desktop-protocol/dist/wire.js +0 -2
- package/node_modules/@code-yeongyu/senpi-desktop-protocol/dist/wire.js.map +0 -1
- package/node_modules/@code-yeongyu/senpi-desktop-protocol/package.json +0 -45
- package/node_modules/@code-yeongyu/senpi-desktop-service/README.md +0 -27
- package/node_modules/@code-yeongyu/senpi-desktop-service/dist/index.d.ts +0 -2
- package/node_modules/@code-yeongyu/senpi-desktop-service/dist/index.d.ts.map +0 -1
- package/node_modules/@code-yeongyu/senpi-desktop-service/dist/index.js +0 -2
- package/node_modules/@code-yeongyu/senpi-desktop-service/dist/index.js.map +0 -1
- package/node_modules/@code-yeongyu/senpi-desktop-service/package.json +0 -44
- package/node_modules/@code-yeongyu/senpi-desktop-tool/README.md +0 -20
- package/node_modules/@code-yeongyu/senpi-desktop-tool/dist/index.d.ts +0 -2
- package/node_modules/@code-yeongyu/senpi-desktop-tool/dist/index.d.ts.map +0 -1
- package/node_modules/@code-yeongyu/senpi-desktop-tool/dist/index.js +0 -2
- package/node_modules/@code-yeongyu/senpi-desktop-tool/dist/index.js.map +0 -1
- package/node_modules/@code-yeongyu/senpi-desktop-tool/package.json +0 -44
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Provider image rejections (senpi#2170). A provider that rejects an image
|
|
3
|
+
* rejects every later request that replays it, and `dropFailedAssistantTurns`
|
|
4
|
+
* removes the failed turn but not the image, so one bad image bricked the
|
|
5
|
+
* session. The failed assistant turn is persisted with its error, so the
|
|
6
|
+
* rejection is derived from history on every request (and after a restart):
|
|
7
|
+
* images no successful response had accepted before an image-rejection failure
|
|
8
|
+
* are replaced by a placeholder naming their source.
|
|
9
|
+
*/
|
|
10
|
+
const PROVIDER_IMAGE_REJECTION_PATTERN = /does not represent a valid image|image_parse_error|invalid_image|unsupported image|could not process image|unable to process input image|image_url'?\.?[^\n]*invalid base64|expected a base64-encoded data url|image was specified using the image\/[a-z]+ media type|unsupported media type for base64 image|invalid data url for image/i;
|
|
11
|
+
export function isProviderImageRejection(errorMessage) {
|
|
12
|
+
return errorMessage !== undefined && PROVIDER_IMAGE_REJECTION_PATTERN.test(errorMessage);
|
|
13
|
+
}
|
|
14
|
+
export function rejectedImagePlaceholder(source) {
|
|
15
|
+
return `[Image omitted: the provider rejected this image (${source}) as invalid, so it is no longer sent with the conversation.]`;
|
|
16
|
+
}
|
|
17
|
+
function field(message, key) {
|
|
18
|
+
return key in message ? message[key] : undefined;
|
|
19
|
+
}
|
|
20
|
+
function contentBlocks(message) {
|
|
21
|
+
const content = field(message, "content");
|
|
22
|
+
return Array.isArray(content) ? content : [];
|
|
23
|
+
}
|
|
24
|
+
function toolCallSource(block) {
|
|
25
|
+
const args = block.arguments;
|
|
26
|
+
if (typeof args !== "object" || args === null)
|
|
27
|
+
return undefined;
|
|
28
|
+
const record = args;
|
|
29
|
+
for (const key of ["path", "file_path", "filePath"]) {
|
|
30
|
+
const value = record[key];
|
|
31
|
+
if (typeof value === "string" && value.trim().length > 0)
|
|
32
|
+
return value;
|
|
33
|
+
}
|
|
34
|
+
return undefined;
|
|
35
|
+
}
|
|
36
|
+
function imageSource(message, toolCallSources) {
|
|
37
|
+
if (message.role !== "toolResult")
|
|
38
|
+
return "an attached image";
|
|
39
|
+
const toolCallId = field(message, "toolCallId");
|
|
40
|
+
const known = typeof toolCallId === "string" ? toolCallSources.get(toolCallId) : undefined;
|
|
41
|
+
if (known)
|
|
42
|
+
return known;
|
|
43
|
+
const toolName = field(message, "toolName");
|
|
44
|
+
return typeof toolName === "string" ? `${toolName} tool result` : "a tool result";
|
|
45
|
+
}
|
|
46
|
+
/** Rejected images keyed by the index of the failed assistant turn that rejected them. */
|
|
47
|
+
function scanRejectedImages(messages) {
|
|
48
|
+
const toolCallSources = new Map();
|
|
49
|
+
const rejections = new Map();
|
|
50
|
+
let unconfirmed = [];
|
|
51
|
+
messages.forEach((message, messageIndex) => {
|
|
52
|
+
if (message.role === "assistant") {
|
|
53
|
+
for (const block of contentBlocks(message)) {
|
|
54
|
+
const source = block.type === "toolCall" ? toolCallSource(block) : undefined;
|
|
55
|
+
if (source && typeof block.id === "string")
|
|
56
|
+
toolCallSources.set(block.id, source);
|
|
57
|
+
}
|
|
58
|
+
const stopReason = field(message, "stopReason");
|
|
59
|
+
const errorMessage = field(message, "errorMessage");
|
|
60
|
+
if (stopReason === "error" &&
|
|
61
|
+
isProviderImageRejection(typeof errorMessage === "string" ? errorMessage : undefined)) {
|
|
62
|
+
if (unconfirmed.length > 0)
|
|
63
|
+
rejections.set(messageIndex, unconfirmed);
|
|
64
|
+
unconfirmed = [];
|
|
65
|
+
}
|
|
66
|
+
else if (stopReason !== "error" && stopReason !== "aborted") {
|
|
67
|
+
unconfirmed = [];
|
|
68
|
+
}
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
if (message.role !== "user" && message.role !== "toolResult")
|
|
72
|
+
return;
|
|
73
|
+
contentBlocks(message).forEach((block, blockIndex) => {
|
|
74
|
+
if (block.type === "image") {
|
|
75
|
+
unconfirmed.push({ messageIndex, blockIndex, source: imageSource(message, toolCallSources) });
|
|
76
|
+
}
|
|
77
|
+
});
|
|
78
|
+
});
|
|
79
|
+
return rejections;
|
|
80
|
+
}
|
|
81
|
+
export function rejectedImageSources(messages, failedTurn) {
|
|
82
|
+
const failedIndex = messages.lastIndexOf(failedTurn);
|
|
83
|
+
return (scanRejectedImages(messages).get(failedIndex) ?? []).map((image) => image.source);
|
|
84
|
+
}
|
|
85
|
+
export function omitProviderRejectedImages(messages) {
|
|
86
|
+
const rejected = new Map();
|
|
87
|
+
for (const images of scanRejectedImages(messages).values()) {
|
|
88
|
+
for (const image of images) {
|
|
89
|
+
const blocks = rejected.get(image.messageIndex) ?? new Map();
|
|
90
|
+
blocks.set(image.blockIndex, image.source);
|
|
91
|
+
rejected.set(image.messageIndex, blocks);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
if (rejected.size === 0)
|
|
95
|
+
return messages;
|
|
96
|
+
return messages.map((message, messageIndex) => {
|
|
97
|
+
const blocks = rejected.get(messageIndex);
|
|
98
|
+
if (!blocks)
|
|
99
|
+
return message;
|
|
100
|
+
const content = contentBlocks(message).map((block, blockIndex) => {
|
|
101
|
+
const source = blocks.get(blockIndex);
|
|
102
|
+
return source === undefined ? block : { type: "text", text: rejectedImagePlaceholder(source) };
|
|
103
|
+
});
|
|
104
|
+
return { ...message, content };
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
//# sourceMappingURL=provider-rejected-images.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"provider-rejected-images.js","sourceRoot":"","sources":["../../src/core/provider-rejected-images.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Provider image rejections (senpi#2170). A provider that rejects an image\n * rejects every later request that replays it, and `dropFailedAssistantTurns`\n * removes the failed turn but not the image, so one bad image bricked the\n * session. The failed assistant turn is persisted with its error, so the\n * rejection is derived from history on every request (and after a restart):\n * images no successful response had accepted before an image-rejection failure\n * are replaced by a placeholder naming their source.\n */\n\nconst PROVIDER_IMAGE_REJECTION_PATTERN =\n\t/does not represent a valid image|image_parse_error|invalid_image|unsupported image|could not process image|unable to process input image|image_url'?\\.?[^\\n]*invalid base64|expected a base64-encoded data url|image was specified using the image\\/[a-z]+ media type|unsupported media type for base64 image|invalid data url for image/i;\n\ninterface TurnBlock {\n\ttype: string;\n\tid?: unknown;\n\targuments?: unknown;\n}\n\ninterface TurnMessage {\n\trole: string;\n}\n\ninterface RejectedImage {\n\tmessageIndex: number;\n\tblockIndex: number;\n\tsource: string;\n}\n\nexport function isProviderImageRejection(errorMessage: string | undefined): boolean {\n\treturn errorMessage !== undefined && PROVIDER_IMAGE_REJECTION_PATTERN.test(errorMessage);\n}\n\nexport function rejectedImagePlaceholder(source: string): string {\n\treturn `[Image omitted: the provider rejected this image (${source}) as invalid, so it is no longer sent with the conversation.]`;\n}\n\nfunction field(message: TurnMessage, key: string): unknown {\n\treturn key in message ? (message as unknown as Record<string, unknown>)[key] : undefined;\n}\n\nfunction contentBlocks(message: TurnMessage): readonly TurnBlock[] {\n\tconst content = field(message, \"content\");\n\treturn Array.isArray(content) ? (content as TurnBlock[]) : [];\n}\n\nfunction toolCallSource(block: TurnBlock): string | undefined {\n\tconst args = block.arguments;\n\tif (typeof args !== \"object\" || args === null) return undefined;\n\tconst record = args as Record<string, unknown>;\n\tfor (const key of [\"path\", \"file_path\", \"filePath\"]) {\n\t\tconst value = record[key];\n\t\tif (typeof value === \"string\" && value.trim().length > 0) return value;\n\t}\n\treturn undefined;\n}\n\nfunction imageSource(message: TurnMessage, toolCallSources: ReadonlyMap<string, string>): string {\n\tif (message.role !== \"toolResult\") return \"an attached image\";\n\tconst toolCallId = field(message, \"toolCallId\");\n\tconst known = typeof toolCallId === \"string\" ? toolCallSources.get(toolCallId) : undefined;\n\tif (known) return known;\n\tconst toolName = field(message, \"toolName\");\n\treturn typeof toolName === \"string\" ? `${toolName} tool result` : \"a tool result\";\n}\n\n/** Rejected images keyed by the index of the failed assistant turn that rejected them. */\nfunction scanRejectedImages(messages: readonly TurnMessage[]): Map<number, RejectedImage[]> {\n\tconst toolCallSources = new Map<string, string>();\n\tconst rejections = new Map<number, RejectedImage[]>();\n\tlet unconfirmed: RejectedImage[] = [];\n\tmessages.forEach((message, messageIndex) => {\n\t\tif (message.role === \"assistant\") {\n\t\t\tfor (const block of contentBlocks(message)) {\n\t\t\t\tconst source = block.type === \"toolCall\" ? toolCallSource(block) : undefined;\n\t\t\t\tif (source && typeof block.id === \"string\") toolCallSources.set(block.id, source);\n\t\t\t}\n\t\t\tconst stopReason = field(message, \"stopReason\");\n\t\t\tconst errorMessage = field(message, \"errorMessage\");\n\t\t\tif (\n\t\t\t\tstopReason === \"error\" &&\n\t\t\t\tisProviderImageRejection(typeof errorMessage === \"string\" ? errorMessage : undefined)\n\t\t\t) {\n\t\t\t\tif (unconfirmed.length > 0) rejections.set(messageIndex, unconfirmed);\n\t\t\t\tunconfirmed = [];\n\t\t\t} else if (stopReason !== \"error\" && stopReason !== \"aborted\") {\n\t\t\t\tunconfirmed = [];\n\t\t\t}\n\t\t\treturn;\n\t\t}\n\t\tif (message.role !== \"user\" && message.role !== \"toolResult\") return;\n\t\tcontentBlocks(message).forEach((block, blockIndex) => {\n\t\t\tif (block.type === \"image\") {\n\t\t\t\tunconfirmed.push({ messageIndex, blockIndex, source: imageSource(message, toolCallSources) });\n\t\t\t}\n\t\t});\n\t});\n\treturn rejections;\n}\n\nexport function rejectedImageSources(messages: readonly TurnMessage[], failedTurn: TurnMessage): string[] {\n\tconst failedIndex = messages.lastIndexOf(failedTurn);\n\treturn (scanRejectedImages(messages).get(failedIndex) ?? []).map((image) => image.source);\n}\n\nexport function omitProviderRejectedImages<T extends TurnMessage>(messages: T[]): T[] {\n\tconst rejected = new Map<number, Map<number, string>>();\n\tfor (const images of scanRejectedImages(messages).values()) {\n\t\tfor (const image of images) {\n\t\t\tconst blocks = rejected.get(image.messageIndex) ?? new Map<number, string>();\n\t\t\tblocks.set(image.blockIndex, image.source);\n\t\t\trejected.set(image.messageIndex, blocks);\n\t\t}\n\t}\n\tif (rejected.size === 0) return messages;\n\treturn messages.map((message, messageIndex) => {\n\t\tconst blocks = rejected.get(messageIndex);\n\t\tif (!blocks) return message;\n\t\tconst content = contentBlocks(message).map((block, blockIndex) => {\n\t\t\tconst source = blocks.get(blockIndex);\n\t\t\treturn source === undefined ? block : { type: \"text\", text: rejectedImagePlaceholder(source) };\n\t\t});\n\t\treturn { ...message, content } as T;\n\t});\n}\n"]}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export declare const REPOSITORY_IDENTITY_ENTRY_TYPE = "repository-identity";
|
|
2
|
+
export interface RepositoryIdentity {
|
|
3
|
+
readonly rootCommits: readonly string[];
|
|
4
|
+
readonly originUrl?: string;
|
|
5
|
+
}
|
|
6
|
+
export type RepositoryMatch = "same" | "different" | "unknown";
|
|
7
|
+
export type GitRunner = (dir: string, args: readonly string[]) => Promise<string | undefined>;
|
|
8
|
+
export declare function readRepositoryIdentity(dir: string, run?: GitRunner): Promise<RepositoryIdentity | undefined>;
|
|
9
|
+
/** `git@host:owner/repo.git`, `ssh://git@host:22/owner/repo`, and `https://host/owner/repo` all become `host/owner/repo`. */
|
|
10
|
+
export declare function normalizeRemoteUrl(url: string): string;
|
|
11
|
+
/**
|
|
12
|
+
* Two checkouts are the same repository when their histories share a root commit. Without commits on
|
|
13
|
+
* either side the origin remote decides. Anything less is "unknown", never a guess.
|
|
14
|
+
*/
|
|
15
|
+
export declare function compareRepositoryIdentities(a: RepositoryIdentity | undefined, b: RepositoryIdentity | undefined): RepositoryMatch;
|
|
16
|
+
export declare function parseRepositoryIdentity(value: unknown): RepositoryIdentity | undefined;
|
|
17
|
+
export declare function sameRepositoryIdentity(a: RepositoryIdentity | undefined, b: RepositoryIdentity | undefined): boolean;
|
|
18
|
+
//# sourceMappingURL=repository-identity.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"repository-identity.d.ts","sourceRoot":"","sources":["../../src/core/repository-identity.ts"],"names":[],"mappings":"AAEA,eAAO,MAAM,8BAA8B,wBAAwB,CAAC;AAOpE,MAAM,WAAW,kBAAkB;IAClC,QAAQ,CAAC,WAAW,EAAE,SAAS,MAAM,EAAE,CAAC;IACxC,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAC;CAC5B;AAED,MAAM,MAAM,eAAe,GAAG,MAAM,GAAG,WAAW,GAAG,SAAS,CAAC;AAE/D,MAAM,MAAM,SAAS,GAAG,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,SAAS,MAAM,EAAE,KAAK,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,CAAC;AAU9F,wBAAsB,sBAAsB,CAC3C,GAAG,EAAE,MAAM,EACX,GAAG,GAAE,SAAkB,GACrB,OAAO,CAAC,kBAAkB,GAAG,SAAS,CAAC,CAYzC;AAED,6HAA6H;AAC7H,wBAAgB,kBAAkB,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAatD;AAED;;;GAGG;AACH,wBAAgB,2BAA2B,CAC1C,CAAC,EAAE,kBAAkB,GAAG,SAAS,EACjC,CAAC,EAAE,kBAAkB,GAAG,SAAS,GAC/B,eAAe,CASjB;AAED,wBAAgB,uBAAuB,CAAC,KAAK,EAAE,OAAO,GAAG,kBAAkB,GAAG,SAAS,CAQtF;AAED,wBAAgB,sBAAsB,CAAC,CAAC,EAAE,kBAAkB,GAAG,SAAS,EAAE,CAAC,EAAE,kBAAkB,GAAG,SAAS,GAAG,OAAO,CAGpH"}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { execFile } from "node:child_process";
|
|
2
|
+
export const REPOSITORY_IDENTITY_ENTRY_TYPE = "repository-identity";
|
|
3
|
+
const GIT_TIMEOUT_MS = 5_000;
|
|
4
|
+
const COMMIT_HASH = /^[0-9a-f]{40,64}$/;
|
|
5
|
+
// An inherited repository location would make every directory report that one repository.
|
|
6
|
+
const GIT_LOCATION_ENV = new Set(["GIT_DIR", "GIT_WORK_TREE", "GIT_COMMON_DIR", "GIT_INDEX_FILE"]);
|
|
7
|
+
const runGit = (dir, args) => new Promise((resolve) => {
|
|
8
|
+
const env = Object.fromEntries(Object.entries(process.env).filter(([name]) => !GIT_LOCATION_ENV.has(name)));
|
|
9
|
+
execFile("git", ["-C", dir, ...args], { env, timeout: GIT_TIMEOUT_MS, windowsHide: true }, (error, stdout) => {
|
|
10
|
+
resolve(error ? undefined : stdout);
|
|
11
|
+
});
|
|
12
|
+
});
|
|
13
|
+
export async function readRepositoryIdentity(dir, run = runGit) {
|
|
14
|
+
const [roots, origin] = await Promise.all([
|
|
15
|
+
run(dir, ["rev-list", "--max-parents=0", "HEAD"]),
|
|
16
|
+
run(dir, ["config", "--get", "remote.origin.url"]),
|
|
17
|
+
]);
|
|
18
|
+
const rootCommits = (roots ?? "")
|
|
19
|
+
.split(/\s+/)
|
|
20
|
+
.filter((line) => COMMIT_HASH.test(line))
|
|
21
|
+
.sort();
|
|
22
|
+
const originUrl = origin?.trim() ? normalizeRemoteUrl(origin) : undefined;
|
|
23
|
+
if (rootCommits.length === 0 && originUrl === undefined)
|
|
24
|
+
return undefined;
|
|
25
|
+
return originUrl === undefined ? { rootCommits } : { rootCommits, originUrl };
|
|
26
|
+
}
|
|
27
|
+
/** `git@host:owner/repo.git`, `ssh://git@host:22/owner/repo`, and `https://host/owner/repo` all become `host/owner/repo`. */
|
|
28
|
+
export function normalizeRemoteUrl(url) {
|
|
29
|
+
const trimmed = url.trim();
|
|
30
|
+
const scpLike = /^(?:[^@/\s]+@)?([^:/\s]+):(?!\/)(.+)$/.exec(trimmed);
|
|
31
|
+
const withoutScheme = scpLike
|
|
32
|
+
? `${scpLike[1]}/${scpLike[2]}`
|
|
33
|
+
: trimmed
|
|
34
|
+
.replace(/^[a-z][a-z0-9+.-]*:\/\//i, "")
|
|
35
|
+
.replace(/^[^@/]+@/, "")
|
|
36
|
+
.replace(/^([^/:]+):\d+\//, "$1/");
|
|
37
|
+
return withoutScheme
|
|
38
|
+
.replace(/\/+$/, "")
|
|
39
|
+
.replace(/\.git$/i, "")
|
|
40
|
+
.toLowerCase();
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Two checkouts are the same repository when their histories share a root commit. Without commits on
|
|
44
|
+
* either side the origin remote decides. Anything less is "unknown", never a guess.
|
|
45
|
+
*/
|
|
46
|
+
export function compareRepositoryIdentities(a, b) {
|
|
47
|
+
if (a === undefined || b === undefined)
|
|
48
|
+
return "unknown";
|
|
49
|
+
if (a.rootCommits.length > 0 && b.rootCommits.length > 0) {
|
|
50
|
+
return a.rootCommits.some((commit) => b.rootCommits.includes(commit)) ? "same" : "different";
|
|
51
|
+
}
|
|
52
|
+
if (a.originUrl !== undefined && b.originUrl !== undefined) {
|
|
53
|
+
return a.originUrl === b.originUrl ? "same" : "different";
|
|
54
|
+
}
|
|
55
|
+
return "unknown";
|
|
56
|
+
}
|
|
57
|
+
export function parseRepositoryIdentity(value) {
|
|
58
|
+
if (typeof value !== "object" || value === null)
|
|
59
|
+
return undefined;
|
|
60
|
+
const { rootCommits, originUrl } = value;
|
|
61
|
+
if (!Array.isArray(rootCommits) || !rootCommits.every((commit) => typeof commit === "string"))
|
|
62
|
+
return undefined;
|
|
63
|
+
if (originUrl !== undefined && typeof originUrl !== "string")
|
|
64
|
+
return undefined;
|
|
65
|
+
const commits = rootCommits.filter((commit) => COMMIT_HASH.test(commit)).sort();
|
|
66
|
+
if (commits.length === 0 && originUrl === undefined)
|
|
67
|
+
return undefined;
|
|
68
|
+
return originUrl === undefined ? { rootCommits: commits } : { rootCommits: commits, originUrl };
|
|
69
|
+
}
|
|
70
|
+
export function sameRepositoryIdentity(a, b) {
|
|
71
|
+
if (a === undefined || b === undefined)
|
|
72
|
+
return a === b;
|
|
73
|
+
return a.originUrl === b.originUrl && a.rootCommits.join(",") === b.rootCommits.join(",");
|
|
74
|
+
}
|
|
75
|
+
//# sourceMappingURL=repository-identity.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"repository-identity.js","sourceRoot":"","sources":["../../src/core/repository-identity.ts"],"names":[],"mappings":"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","sourcesContent":["import { execFile } from \"node:child_process\";\n\nexport const REPOSITORY_IDENTITY_ENTRY_TYPE = \"repository-identity\";\n\nconst GIT_TIMEOUT_MS = 5_000;\nconst COMMIT_HASH = /^[0-9a-f]{40,64}$/;\n// An inherited repository location would make every directory report that one repository.\nconst GIT_LOCATION_ENV = new Set([\"GIT_DIR\", \"GIT_WORK_TREE\", \"GIT_COMMON_DIR\", \"GIT_INDEX_FILE\"]);\n\nexport interface RepositoryIdentity {\n\treadonly rootCommits: readonly string[];\n\treadonly originUrl?: string;\n}\n\nexport type RepositoryMatch = \"same\" | \"different\" | \"unknown\";\n\nexport type GitRunner = (dir: string, args: readonly string[]) => Promise<string | undefined>;\n\nconst runGit: GitRunner = (dir, args) =>\n\tnew Promise((resolve) => {\n\t\tconst env = Object.fromEntries(Object.entries(process.env).filter(([name]) => !GIT_LOCATION_ENV.has(name)));\n\t\texecFile(\"git\", [\"-C\", dir, ...args], { env, timeout: GIT_TIMEOUT_MS, windowsHide: true }, (error, stdout) => {\n\t\t\tresolve(error ? undefined : stdout);\n\t\t});\n\t});\n\nexport async function readRepositoryIdentity(\n\tdir: string,\n\trun: GitRunner = runGit,\n): Promise<RepositoryIdentity | undefined> {\n\tconst [roots, origin] = await Promise.all([\n\t\trun(dir, [\"rev-list\", \"--max-parents=0\", \"HEAD\"]),\n\t\trun(dir, [\"config\", \"--get\", \"remote.origin.url\"]),\n\t]);\n\tconst rootCommits = (roots ?? \"\")\n\t\t.split(/\\s+/)\n\t\t.filter((line) => COMMIT_HASH.test(line))\n\t\t.sort();\n\tconst originUrl = origin?.trim() ? normalizeRemoteUrl(origin) : undefined;\n\tif (rootCommits.length === 0 && originUrl === undefined) return undefined;\n\treturn originUrl === undefined ? { rootCommits } : { rootCommits, originUrl };\n}\n\n/** `git@host:owner/repo.git`, `ssh://git@host:22/owner/repo`, and `https://host/owner/repo` all become `host/owner/repo`. */\nexport function normalizeRemoteUrl(url: string): string {\n\tconst trimmed = url.trim();\n\tconst scpLike = /^(?:[^@/\\s]+@)?([^:/\\s]+):(?!\\/)(.+)$/.exec(trimmed);\n\tconst withoutScheme = scpLike\n\t\t? `${scpLike[1]}/${scpLike[2]}`\n\t\t: trimmed\n\t\t\t\t.replace(/^[a-z][a-z0-9+.-]*:\\/\\//i, \"\")\n\t\t\t\t.replace(/^[^@/]+@/, \"\")\n\t\t\t\t.replace(/^([^/:]+):\\d+\\//, \"$1/\");\n\treturn withoutScheme\n\t\t.replace(/\\/+$/, \"\")\n\t\t.replace(/\\.git$/i, \"\")\n\t\t.toLowerCase();\n}\n\n/**\n * Two checkouts are the same repository when their histories share a root commit. Without commits on\n * either side the origin remote decides. Anything less is \"unknown\", never a guess.\n */\nexport function compareRepositoryIdentities(\n\ta: RepositoryIdentity | undefined,\n\tb: RepositoryIdentity | undefined,\n): RepositoryMatch {\n\tif (a === undefined || b === undefined) return \"unknown\";\n\tif (a.rootCommits.length > 0 && b.rootCommits.length > 0) {\n\t\treturn a.rootCommits.some((commit) => b.rootCommits.includes(commit)) ? \"same\" : \"different\";\n\t}\n\tif (a.originUrl !== undefined && b.originUrl !== undefined) {\n\t\treturn a.originUrl === b.originUrl ? \"same\" : \"different\";\n\t}\n\treturn \"unknown\";\n}\n\nexport function parseRepositoryIdentity(value: unknown): RepositoryIdentity | undefined {\n\tif (typeof value !== \"object\" || value === null) return undefined;\n\tconst { rootCommits, originUrl } = value as { rootCommits?: unknown; originUrl?: unknown };\n\tif (!Array.isArray(rootCommits) || !rootCommits.every((commit) => typeof commit === \"string\")) return undefined;\n\tif (originUrl !== undefined && typeof originUrl !== \"string\") return undefined;\n\tconst commits = rootCommits.filter((commit) => COMMIT_HASH.test(commit)).sort();\n\tif (commits.length === 0 && originUrl === undefined) return undefined;\n\treturn originUrl === undefined ? { rootCommits: commits } : { rootCommits: commits, originUrl };\n}\n\nexport function sameRepositoryIdentity(a: RepositoryIdentity | undefined, b: RepositoryIdentity | undefined): boolean {\n\tif (a === undefined || b === undefined) return a === b;\n\treturn a.originUrl === b.originUrl && a.rootCommits.join(\",\") === b.rootCommits.join(\",\");\n}\n"]}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { type RepositoryIdentity, type RepositoryMatch } from "./repository-identity.ts";
|
|
2
|
+
export declare function readRecordedRepositoryIdentity(sessionFile: string): RepositoryIdentity | undefined;
|
|
3
|
+
/**
|
|
4
|
+
* Whether `cwd` is the git repository a session was recorded in. The session side prefers the live
|
|
5
|
+
* checkout at its recorded cwd and falls back to the identity the session recorded, because a moved
|
|
6
|
+
* repository leaves nothing at the old path to ask.
|
|
7
|
+
*/
|
|
8
|
+
export declare function classifySessionRepository(sessionFile: string, sessionCwd: string, cwd: string, read?: (dir: string) => Promise<RepositoryIdentity | undefined>): Promise<RepositoryMatch>;
|
|
9
|
+
export declare function readSessionCwd(sessionFile: string): string | undefined;
|
|
10
|
+
/**
|
|
11
|
+
* Moves a session recorded under another project into `targetCwd`'s session directory and points its
|
|
12
|
+
* header at `targetCwd`. The id, file name, every entry after the header, and the session's extension
|
|
13
|
+
* sidecars are kept. The relocated copy is complete before the source is removed, so an interrupted
|
|
14
|
+
* rebind leaves the session readable in at least one place. Returns the session file's new path.
|
|
15
|
+
*/
|
|
16
|
+
export declare function rebindSessionFile(sourcePath: string, targetCwd: string, sessionDir?: string): string;
|
|
17
|
+
//# sourceMappingURL=session-rebind.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"session-rebind.d.ts","sourceRoot":"","sources":["../../src/core/session-rebind.ts"],"names":[],"mappings":"AAGA,OAAO,EAIN,KAAK,kBAAkB,EACvB,KAAK,eAAe,EAEpB,MAAM,0BAA0B,CAAC;AASlC,wBAAgB,8BAA8B,CAAC,WAAW,EAAE,MAAM,GAAG,kBAAkB,GAAG,SAAS,CAQlG;AAED;;;;GAIG;AACH,wBAAsB,yBAAyB,CAC9C,WAAW,EAAE,MAAM,EACnB,UAAU,EAAE,MAAM,EAClB,GAAG,EAAE,MAAM,EACX,IAAI,GAAE,CAAC,GAAG,EAAE,MAAM,KAAK,OAAO,CAAC,kBAAkB,GAAG,SAAS,CAA0B,GACrF,OAAO,CAAC,eAAe,CAAC,CAM1B;AAED,wBAAgB,cAAc,CAAC,WAAW,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAGtE;AAED;;;;;GAKG;AACH,wBAAgB,iBAAiB,CAAC,UAAU,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,UAAU,CAAC,EAAE,MAAM,GAAG,MAAM,CAkBpG"}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { cpSync, existsSync, mkdirSync, readdirSync, readFileSync, renameSync, rmSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { basename, dirname, join } from "node:path";
|
|
3
|
+
import { normalizePath, resolvePath } from "../utils/paths.js";
|
|
4
|
+
import { compareRepositoryIdentities, parseRepositoryIdentity, REPOSITORY_IDENTITY_ENTRY_TYPE, readRepositoryIdentity, } from "./repository-identity.js";
|
|
5
|
+
import { getDefaultSessionDir, loadEntriesFromFile } from "./session-manager.js";
|
|
6
|
+
import { encodedSessionId } from "./session-sidecar-store.js";
|
|
7
|
+
export function readRecordedRepositoryIdentity(sessionFile) {
|
|
8
|
+
let recorded;
|
|
9
|
+
for (const entry of loadEntriesFromFile(sessionFile)) {
|
|
10
|
+
if (entry.type === "custom" && entry.customType === REPOSITORY_IDENTITY_ENTRY_TYPE) {
|
|
11
|
+
recorded = parseRepositoryIdentity(entry.data) ?? recorded;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
return recorded;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Whether `cwd` is the git repository a session was recorded in. The session side prefers the live
|
|
18
|
+
* checkout at its recorded cwd and falls back to the identity the session recorded, because a moved
|
|
19
|
+
* repository leaves nothing at the old path to ask.
|
|
20
|
+
*/
|
|
21
|
+
export async function classifySessionRepository(sessionFile, sessionCwd, cwd, read = readRepositoryIdentity) {
|
|
22
|
+
const [current, live] = await Promise.all([
|
|
23
|
+
read(cwd),
|
|
24
|
+
existsSync(sessionCwd) ? read(sessionCwd) : Promise.resolve(undefined),
|
|
25
|
+
]);
|
|
26
|
+
return compareRepositoryIdentities(live ?? readRecordedRepositoryIdentity(sessionFile), current);
|
|
27
|
+
}
|
|
28
|
+
export function readSessionCwd(sessionFile) {
|
|
29
|
+
const { cwd } = readHeaderLine(resolvePath(sessionFile)).header;
|
|
30
|
+
return typeof cwd === "string" && cwd !== "" ? cwd : undefined;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Moves a session recorded under another project into `targetCwd`'s session directory and points its
|
|
34
|
+
* header at `targetCwd`. The id, file name, every entry after the header, and the session's extension
|
|
35
|
+
* sidecars are kept. The relocated copy is complete before the source is removed, so an interrupted
|
|
36
|
+
* rebind leaves the session readable in at least one place. Returns the session file's new path.
|
|
37
|
+
*/
|
|
38
|
+
export function rebindSessionFile(sourcePath, targetCwd, sessionDir) {
|
|
39
|
+
const source = resolvePath(sourcePath);
|
|
40
|
+
const cwd = resolvePath(targetCwd);
|
|
41
|
+
const { header, rest } = readHeaderLine(source);
|
|
42
|
+
const targetDir = sessionDir ? normalizePath(sessionDir) : getDefaultSessionDir(cwd);
|
|
43
|
+
const target = join(targetDir, basename(source));
|
|
44
|
+
if (target !== source && existsSync(target)) {
|
|
45
|
+
throw new Error(`Cannot rebind session: ${target} already exists`);
|
|
46
|
+
}
|
|
47
|
+
mkdirSync(targetDir, { recursive: true });
|
|
48
|
+
const temp = join(targetDir, `.${basename(source)}.rebind-${process.pid}.tmp`);
|
|
49
|
+
writeFileSync(temp, `${JSON.stringify({ ...header, cwd })}\n${rest}`, { flag: "wx" });
|
|
50
|
+
renameSync(temp, target);
|
|
51
|
+
if (target !== source) {
|
|
52
|
+
moveSessionSidecars(dirname(source), targetDir, header.id);
|
|
53
|
+
rmSync(source, { force: true });
|
|
54
|
+
}
|
|
55
|
+
return target;
|
|
56
|
+
}
|
|
57
|
+
function readHeaderLine(sessionFile) {
|
|
58
|
+
const content = readFileSync(sessionFile, "utf8");
|
|
59
|
+
const newline = content.indexOf("\n");
|
|
60
|
+
const firstLine = newline === -1 ? content : content.slice(0, newline);
|
|
61
|
+
let parsed;
|
|
62
|
+
try {
|
|
63
|
+
parsed = JSON.parse(firstLine);
|
|
64
|
+
}
|
|
65
|
+
catch {
|
|
66
|
+
parsed = undefined;
|
|
67
|
+
}
|
|
68
|
+
if (!isSessionHeader(parsed)) {
|
|
69
|
+
throw new Error(`Cannot rebind session: ${sessionFile} does not start with a session header`);
|
|
70
|
+
}
|
|
71
|
+
return { header: parsed, rest: newline === -1 ? "" : content.slice(newline + 1) };
|
|
72
|
+
}
|
|
73
|
+
function isSessionHeader(value) {
|
|
74
|
+
return (typeof value === "object" &&
|
|
75
|
+
value !== null &&
|
|
76
|
+
value.type === "session" &&
|
|
77
|
+
typeof value.id === "string");
|
|
78
|
+
}
|
|
79
|
+
// Per-session extension state lives at `<sessionDir>/extensions/<extension>/<encoded id>.json`
|
|
80
|
+
// (goal, loop, terminal monitors); without it a rebound session would lose its goal and monitors.
|
|
81
|
+
function moveSessionSidecars(fromDir, toDir, sessionId) {
|
|
82
|
+
const extensionsDir = join(fromDir, "extensions");
|
|
83
|
+
const sidecarName = `${encodedSessionId(sessionId)}.json`;
|
|
84
|
+
if (existsSync(extensionsDir)) {
|
|
85
|
+
for (const extension of readdirSync(extensionsDir, { withFileTypes: true })) {
|
|
86
|
+
if (!extension.isDirectory())
|
|
87
|
+
continue;
|
|
88
|
+
const from = join(extensionsDir, extension.name, sidecarName);
|
|
89
|
+
if (existsSync(from))
|
|
90
|
+
movePath(from, join(toDir, "extensions", extension.name, sidecarName));
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
const blobs = join(fromDir, "resident-blobs", sessionId);
|
|
94
|
+
if (existsSync(blobs))
|
|
95
|
+
movePath(blobs, join(toDir, "resident-blobs", sessionId));
|
|
96
|
+
}
|
|
97
|
+
function movePath(from, to) {
|
|
98
|
+
if (existsSync(to))
|
|
99
|
+
return;
|
|
100
|
+
mkdirSync(dirname(to), { recursive: true });
|
|
101
|
+
try {
|
|
102
|
+
renameSync(from, to);
|
|
103
|
+
}
|
|
104
|
+
catch (error) {
|
|
105
|
+
if (error.code !== "EXDEV")
|
|
106
|
+
throw error;
|
|
107
|
+
cpSync(from, to, { recursive: true });
|
|
108
|
+
rmSync(from, { recursive: true, force: true });
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
//# sourceMappingURL=session-rebind.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"session-rebind.js","sourceRoot":"","sources":["../../src/core/session-rebind.ts"],"names":[],"mappings":"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","sourcesContent":["import { cpSync, existsSync, mkdirSync, readdirSync, readFileSync, renameSync, rmSync, writeFileSync } from \"node:fs\";\nimport { basename, dirname, join } from \"node:path\";\nimport { normalizePath, resolvePath } from \"../utils/paths.ts\";\nimport {\n\tcompareRepositoryIdentities,\n\tparseRepositoryIdentity,\n\tREPOSITORY_IDENTITY_ENTRY_TYPE,\n\ttype RepositoryIdentity,\n\ttype RepositoryMatch,\n\treadRepositoryIdentity,\n} from \"./repository-identity.ts\";\nimport { getDefaultSessionDir, loadEntriesFromFile } from \"./session-manager.ts\";\nimport { encodedSessionId } from \"./session-sidecar-store.ts\";\n\ninterface SessionHeaderLine {\n\treadonly header: { readonly type: \"session\"; readonly id: string; readonly cwd?: unknown };\n\treadonly rest: string;\n}\n\nexport function readRecordedRepositoryIdentity(sessionFile: string): RepositoryIdentity | undefined {\n\tlet recorded: RepositoryIdentity | undefined;\n\tfor (const entry of loadEntriesFromFile(sessionFile)) {\n\t\tif (entry.type === \"custom\" && entry.customType === REPOSITORY_IDENTITY_ENTRY_TYPE) {\n\t\t\trecorded = parseRepositoryIdentity(entry.data) ?? recorded;\n\t\t}\n\t}\n\treturn recorded;\n}\n\n/**\n * Whether `cwd` is the git repository a session was recorded in. The session side prefers the live\n * checkout at its recorded cwd and falls back to the identity the session recorded, because a moved\n * repository leaves nothing at the old path to ask.\n */\nexport async function classifySessionRepository(\n\tsessionFile: string,\n\tsessionCwd: string,\n\tcwd: string,\n\tread: (dir: string) => Promise<RepositoryIdentity | undefined> = readRepositoryIdentity,\n): Promise<RepositoryMatch> {\n\tconst [current, live] = await Promise.all([\n\t\tread(cwd),\n\t\texistsSync(sessionCwd) ? read(sessionCwd) : Promise.resolve(undefined),\n\t]);\n\treturn compareRepositoryIdentities(live ?? readRecordedRepositoryIdentity(sessionFile), current);\n}\n\nexport function readSessionCwd(sessionFile: string): string | undefined {\n\tconst { cwd } = readHeaderLine(resolvePath(sessionFile)).header;\n\treturn typeof cwd === \"string\" && cwd !== \"\" ? cwd : undefined;\n}\n\n/**\n * Moves a session recorded under another project into `targetCwd`'s session directory and points its\n * header at `targetCwd`. The id, file name, every entry after the header, and the session's extension\n * sidecars are kept. The relocated copy is complete before the source is removed, so an interrupted\n * rebind leaves the session readable in at least one place. Returns the session file's new path.\n */\nexport function rebindSessionFile(sourcePath: string, targetCwd: string, sessionDir?: string): string {\n\tconst source = resolvePath(sourcePath);\n\tconst cwd = resolvePath(targetCwd);\n\tconst { header, rest } = readHeaderLine(source);\n\tconst targetDir = sessionDir ? normalizePath(sessionDir) : getDefaultSessionDir(cwd);\n\tconst target = join(targetDir, basename(source));\n\tif (target !== source && existsSync(target)) {\n\t\tthrow new Error(`Cannot rebind session: ${target} already exists`);\n\t}\n\tmkdirSync(targetDir, { recursive: true });\n\tconst temp = join(targetDir, `.${basename(source)}.rebind-${process.pid}.tmp`);\n\twriteFileSync(temp, `${JSON.stringify({ ...header, cwd })}\\n${rest}`, { flag: \"wx\" });\n\trenameSync(temp, target);\n\tif (target !== source) {\n\t\tmoveSessionSidecars(dirname(source), targetDir, header.id);\n\t\trmSync(source, { force: true });\n\t}\n\treturn target;\n}\n\nfunction readHeaderLine(sessionFile: string): SessionHeaderLine {\n\tconst content = readFileSync(sessionFile, \"utf8\");\n\tconst newline = content.indexOf(\"\\n\");\n\tconst firstLine = newline === -1 ? content : content.slice(0, newline);\n\tlet parsed: unknown;\n\ttry {\n\t\tparsed = JSON.parse(firstLine);\n\t} catch {\n\t\tparsed = undefined;\n\t}\n\tif (!isSessionHeader(parsed)) {\n\t\tthrow new Error(`Cannot rebind session: ${sessionFile} does not start with a session header`);\n\t}\n\treturn { header: parsed, rest: newline === -1 ? \"\" : content.slice(newline + 1) };\n}\n\nfunction isSessionHeader(value: unknown): value is SessionHeaderLine[\"header\"] {\n\treturn (\n\t\ttypeof value === \"object\" &&\n\t\tvalue !== null &&\n\t\t(value as { type?: unknown }).type === \"session\" &&\n\t\ttypeof (value as { id?: unknown }).id === \"string\"\n\t);\n}\n\n// Per-session extension state lives at `<sessionDir>/extensions/<extension>/<encoded id>.json`\n// (goal, loop, terminal monitors); without it a rebound session would lose its goal and monitors.\nfunction moveSessionSidecars(fromDir: string, toDir: string, sessionId: string): void {\n\tconst extensionsDir = join(fromDir, \"extensions\");\n\tconst sidecarName = `${encodedSessionId(sessionId)}.json`;\n\tif (existsSync(extensionsDir)) {\n\t\tfor (const extension of readdirSync(extensionsDir, { withFileTypes: true })) {\n\t\t\tif (!extension.isDirectory()) continue;\n\t\t\tconst from = join(extensionsDir, extension.name, sidecarName);\n\t\t\tif (existsSync(from)) movePath(from, join(toDir, \"extensions\", extension.name, sidecarName));\n\t\t}\n\t}\n\tconst blobs = join(fromDir, \"resident-blobs\", sessionId);\n\tif (existsSync(blobs)) movePath(blobs, join(toDir, \"resident-blobs\", sessionId));\n}\n\nfunction movePath(from: string, to: string): void {\n\tif (existsSync(to)) return;\n\tmkdirSync(dirname(to), { recursive: true });\n\ttry {\n\t\trenameSync(from, to);\n\t} catch (error) {\n\t\tif ((error as NodeJS.ErrnoException).code !== \"EXDEV\") throw error;\n\t\tcpSync(from, to, { recursive: true });\n\t\trmSync(from, { recursive: true, force: true });\n\t}\n}\n"]}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"session-title-generator.d.ts","sourceRoot":"","sources":["../../src/core/session-title-generator.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,+BAA+B,CAAC;AAC9D,OAAO,KAAK,EACX,GAAG,EAGH,KAAK,EACL,mBAAmB,
|
|
1
|
+
{"version":3,"file":"session-title-generator.d.ts","sourceRoot":"","sources":["../../src/core/session-title-generator.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,+BAA+B,CAAC;AAC9D,OAAO,KAAK,EACX,GAAG,EAGH,KAAK,EACL,mBAAmB,EAGnB,MAAM,8BAA8B,CAAC;AACtC,OAAO,EAAkB,KAAK,WAAW,EAAsB,MAAM,8BAA8B,CAAC;AAGpG,UAAU,gBAAgB;IACzB,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC1C,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAC7C,QAAQ,CAAC,GAAG,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CACtC;AAED,UAAU,2BAA2B;IACpC,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,KAAK,EAAE,KAAK,CAAC,GAAG,CAAC,CAAC;IAC3B,QAAQ,CAAC,IAAI,EAAE,gBAAgB,CAAC;IAChC,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,WAAW,CAAC,EAAE,mBAAmB,CAAC;IAC3C,QAAQ,CAAC,MAAM,CAAC,EAAE,WAAW,CAAC;IAC9B,QAAQ,CAAC,QAAQ,CAAC,EAAE,QAAQ,CAAC;IAC7B,QAAQ,CAAC,KAAK,CAAC,EAAE,WAAW,CAAC;CAC7B;AAmCD;;;;;;;;;GASG;AACH,wBAAgB,uBAAuB,CAAC,QAAQ,EAAE,WAAW,GAAG,WAAW,CAM1E;AAED,wBAAgB,sBAAsB,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAK5D;AAED,wBAAsB,oBAAoB,CAAC,OAAO,EAAE,2BAA2B,GAAG,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,CAqB5G;AA8FD;;;;;GAKG;AACH,wBAAgB,qBAAqB,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAsCzD"}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { completeSimple, retryAssistantCall } from "@earendil-works/pi-ai/compat";
|
|
2
|
+
import { initialTitleReasoning, mandatoryReasoningRetryLevel } from "./session-title-reasoning.js";
|
|
2
3
|
const TITLE_SYSTEM_PROMPT = `Generate a concise title for this coding-agent session.
|
|
3
4
|
|
|
4
5
|
Rules:
|
|
@@ -22,6 +23,10 @@ const LOW_SIGNAL_PROMPTS = new Set([
|
|
|
22
23
|
"yep",
|
|
23
24
|
"nope",
|
|
24
25
|
]);
|
|
26
|
+
const TITLE_MAX_TOKENS = 64;
|
|
27
|
+
// Reasoning tokens count against the completion budget, so a title that must reason needs room
|
|
28
|
+
// for the thinking and the `<title>` answer.
|
|
29
|
+
const REASONING_TITLE_MAX_TOKENS = 1024;
|
|
25
30
|
const MAX_TITLE_RETRIES = 1;
|
|
26
31
|
const MAX_TITLE_RETRY_DELAY_MS = 2000;
|
|
27
32
|
/**
|
|
@@ -56,12 +61,22 @@ export async function generateSessionTitle(options) {
|
|
|
56
61
|
// Titles are cosmetic background work: honor the caller's retry policy so a
|
|
57
62
|
// single transient provider error (e.g. a 529 overloaded stream) does not
|
|
58
63
|
// surface as a scary runtime error. Mirrors completeSummarization().
|
|
59
|
-
const
|
|
64
|
+
const reasoning = initialTitleReasoning(options.model);
|
|
65
|
+
let response = await requestTitle(options, reasoning);
|
|
66
|
+
const retryReasoning = response.stopReason === "error"
|
|
67
|
+
? mandatoryReasoningRetryLevel(options.model, reasoning, response.errorMessage)
|
|
68
|
+
: undefined;
|
|
69
|
+
if (retryReasoning !== undefined) {
|
|
70
|
+
response = await requestTitle(options, retryReasoning);
|
|
71
|
+
}
|
|
60
72
|
if (response.stopReason === "error") {
|
|
61
73
|
throw new Error(humanizeProviderError(response.errorMessage ?? "Session title generation failed"));
|
|
62
74
|
}
|
|
63
75
|
return parseSessionTitle(response);
|
|
64
76
|
}
|
|
77
|
+
function requestTitle(options, reasoning) {
|
|
78
|
+
return retryAssistantCall(() => completeTitle(options.model, buildTitleContext(options.firstPrompt), buildTitleOptions(options, reasoning), options.streamFn), options.retry, options.signal);
|
|
79
|
+
}
|
|
65
80
|
function buildTitleContext(firstPrompt) {
|
|
66
81
|
return {
|
|
67
82
|
systemPrompt: TITLE_SYSTEM_PROMPT,
|
|
@@ -74,13 +89,16 @@ function buildTitleContext(firstPrompt) {
|
|
|
74
89
|
],
|
|
75
90
|
};
|
|
76
91
|
}
|
|
77
|
-
function buildTitleOptions(options) {
|
|
92
|
+
function buildTitleOptions(options, reasoning) {
|
|
78
93
|
const titleOptions = {
|
|
79
94
|
...options.baseOptions,
|
|
80
95
|
sessionId: options.sessionId,
|
|
81
96
|
cacheRetention: options.model.cacheRetention === "none" ? "none" : "short",
|
|
82
|
-
maxTokens:
|
|
97
|
+
maxTokens: reasoning === undefined ? TITLE_MAX_TOKENS : REASONING_TITLE_MAX_TOKENS,
|
|
83
98
|
};
|
|
99
|
+
if (reasoning !== undefined) {
|
|
100
|
+
titleOptions.reasoning = reasoning;
|
|
101
|
+
}
|
|
84
102
|
if (options.auth.apiKey !== undefined) {
|
|
85
103
|
titleOptions.apiKey = options.auth.apiKey;
|
|
86
104
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"session-title-generator.js","sourceRoot":"","sources":["../../src/core/session-title-generator.ts"],"names":[],"mappings":"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","sourcesContent":["import type { StreamFn } from \"@earendil-works/pi-agent-core\";\nimport type {\n\tApi,\n\tAssistantMessage,\n\tContext,\n\tModel,\n\tSimpleStreamOptions,\n\tTextContent,\n} from \"@earendil-works/pi-ai/compat\";\nimport { completeSimple, type RetryPolicy, retryAssistantCall } from \"@earendil-works/pi-ai/compat\";\n\ninterface SessionTitleAuth {\n\treadonly apiKey?: string;\n\treadonly headers?: Record<string, string>;\n\treadonly extraBody?: Record<string, unknown>;\n\treadonly env?: Record<string, string>;\n}\n\ninterface GenerateSessionTitleOptions {\n\treadonly firstPrompt: string;\n\treadonly model: Model<Api>;\n\treadonly auth: SessionTitleAuth;\n\treadonly sessionId: string;\n\treadonly baseOptions?: SimpleStreamOptions;\n\treadonly signal?: AbortSignal;\n\treadonly streamFn?: StreamFn;\n\treadonly retry?: RetryPolicy;\n}\n\nconst TITLE_SYSTEM_PROMPT = `Generate a concise title for this coding-agent session.\n\nRules:\n- Use 3 to 6 words.\n- Prefer concrete nouns and verbs from the user's task.\n- Do not include quotes, punctuation at the end, markdown, or explanations.\n- If the input is only a greeting, acknowledgement, or too vague to title, return <title>none</title>.\n- Respond only as <title>Session Title</title>.`;\n\nconst LOW_SIGNAL_PROMPTS = new Set([\n\t\"hi\",\n\t\"hello\",\n\t\"hey\",\n\t\"yo\",\n\t\"thanks\",\n\t\"thank you\",\n\t\"ok\",\n\t\"okay\",\n\t\"k\",\n\t\"yes\",\n\t\"no\",\n\t\"yep\",\n\t\"nope\",\n]);\n\nconst MAX_TITLE_RETRIES = 1;\nconst MAX_TITLE_RETRY_DELAY_MS = 2000;\n\n/**\n * Narrow the user's retry budget for cosmetic background title generation.\n *\n * A title is not worth the full agent-turn budget: the default policy would\n * keep hitting an already-overloaded provider for ~14s while the user's real\n * turn competes for the same capacity, and a custom `baseDelayMs` could stall\n * it far longer. One retry recovers the common single-blip case; a title that\n * still fails is regenerated at the next turn end anyway. `enabled` stays the\n * user's call, and a smaller configured budget is never inflated.\n */\nexport function sessionTitleRetryPolicy(settings: RetryPolicy): RetryPolicy {\n\treturn {\n\t\tenabled: settings.enabled,\n\t\tmaxRetries: Math.min(settings.maxRetries, MAX_TITLE_RETRIES),\n\t\tbaseDelayMs: Math.min(settings.baseDelayMs, MAX_TITLE_RETRY_DELAY_MS),\n\t};\n}\n\nexport function shouldSkipSessionTitle(text: string): boolean {\n\tconst normalized = text.replace(/\\s+/g, \" \").trim().toLowerCase();\n\tif (normalized.length === 0) return true;\n\tif (normalized.startsWith(\"/\")) return true;\n\treturn LOW_SIGNAL_PROMPTS.has(normalized);\n}\n\nexport async function generateSessionTitle(options: GenerateSessionTitleOptions): Promise<string | undefined> {\n\tif (shouldSkipSessionTitle(options.firstPrompt)) {\n\t\treturn undefined;\n\t}\n\n\t// Titles are cosmetic background work: honor the caller's retry policy so a\n\t// single transient provider error (e.g. a 529 overloaded stream) does not\n\t// surface as a scary runtime error. Mirrors completeSummarization().\n\tconst response = await retryAssistantCall(\n\t\t() =>\n\t\t\tcompleteTitle(\n\t\t\t\toptions.model,\n\t\t\t\tbuildTitleContext(options.firstPrompt),\n\t\t\t\tbuildTitleOptions(options),\n\t\t\t\toptions.streamFn,\n\t\t\t),\n\t\toptions.retry,\n\t\toptions.signal,\n\t);\n\tif (response.stopReason === \"error\") {\n\t\tthrow new Error(humanizeProviderError(response.errorMessage ?? \"Session title generation failed\"));\n\t}\n\treturn parseSessionTitle(response);\n}\n\nfunction buildTitleContext(firstPrompt: string): Context {\n\treturn {\n\t\tsystemPrompt: TITLE_SYSTEM_PROMPT,\n\t\tmessages: [\n\t\t\t{\n\t\t\t\trole: \"user\",\n\t\t\t\tcontent: [{ type: \"text\", text: firstPrompt }],\n\t\t\t\ttimestamp: Date.now(),\n\t\t\t},\n\t\t],\n\t};\n}\n\nfunction buildTitleOptions(options: GenerateSessionTitleOptions): SimpleStreamOptions {\n\tconst titleOptions: SimpleStreamOptions = {\n\t\t...options.baseOptions,\n\t\tsessionId: options.sessionId,\n\t\tcacheRetention: options.model.cacheRetention === \"none\" ? \"none\" : \"short\",\n\t\tmaxTokens: 64,\n\t};\n\tif (options.auth.apiKey !== undefined) {\n\t\ttitleOptions.apiKey = options.auth.apiKey;\n\t}\n\tif (options.auth.headers !== undefined) {\n\t\ttitleOptions.headers = options.auth.headers;\n\t}\n\tif (options.auth.extraBody !== undefined) {\n\t\ttitleOptions.extraBody = options.auth.extraBody;\n\t}\n\tif (options.auth.env !== undefined) {\n\t\ttitleOptions.env = options.auth.env;\n\t}\n\tif (options.signal !== undefined) {\n\t\ttitleOptions.signal = options.signal;\n\t}\n\treturn titleOptions;\n}\n\nasync function completeTitle(\n\tmodel: Model<Api>,\n\tcontext: Context,\n\toptions: SimpleStreamOptions,\n\tstreamFn?: StreamFn,\n): Promise<AssistantMessage> {\n\tif (streamFn === undefined) {\n\t\treturn completeSimple(model, context, options);\n\t}\n\tconst stream = await streamFn(model, context, options);\n\treturn stream.result();\n}\n\nfunction parseSessionTitle(message: AssistantMessage): string | undefined {\n\tconst text = message.content\n\t\t.filter((part): part is TextContent => part.type === \"text\")\n\t\t.map((part) => part.text)\n\t\t.join(\"\")\n\t\t.trim();\n\tconst match = text.match(/<title>\\s*([\\s\\S]*?)\\s*<\\/title>/i);\n\tconst rawTitle = match?.[1];\n\tif (rawTitle === undefined) {\n\t\treturn undefined;\n\t}\n\tconst title = sanitizeTitle(rawTitle);\n\tif (!title || title.toLowerCase() === \"none\") {\n\t\treturn undefined;\n\t}\n\treturn title;\n}\n\n/**\n * Reduce a raw provider error payload (e.g. an Anthropic `{\"type\":\"error\",...}`\n * body, optionally prefixed with an HTTP status code) to a short human-readable\n * line while keeping the error type and request id for follow-up. Non-JSON and\n * unrecognized payloads pass through unchanged.\n */\nexport function humanizeProviderError(raw: string): string {\n\t// `formatProviderError` (packages/ai/src/utils/error-body.ts) emits\n\t// `<status>: <body>` or `<prefix> (<status>): <body>`; Anthropic SSE error\n\t// events arrive as the bare JSON body.\n\tconst statusPrefix = raw.trim().match(/^(?:.*?\\((\\d{3})\\)|(\\d{3}))\\s*:\\s*(?=\\{)/);\n\tconst status = statusPrefix?.[1] ?? statusPrefix?.[2];\n\tconst bodyText = statusPrefix ? raw.trim().slice(statusPrefix[0].length) : raw.trim();\n\tif (!bodyText.startsWith(\"{\")) {\n\t\treturn raw;\n\t}\n\tlet parsed: unknown;\n\ttry {\n\t\tparsed = JSON.parse(bodyText);\n\t} catch {\n\t\treturn raw;\n\t}\n\tconst body = asRecord(parsed);\n\tif (!body) {\n\t\treturn raw;\n\t}\n\tconst nested = asRecord(body.error);\n\tconst message = asNonEmptyString(nested?.message) ?? asNonEmptyString(body.message) ?? asNonEmptyString(body.error);\n\tif (message === undefined) {\n\t\treturn raw;\n\t}\n\tconst details: string[] = [];\n\tconst errorType = asNonEmptyString(nested?.type) ?? asNonEmptyString(body.type);\n\tif (errorType && errorType !== \"error\") {\n\t\tdetails.push(errorType);\n\t}\n\tif (status) {\n\t\tdetails.push(`HTTP ${status}`);\n\t}\n\tconst requestId = asNonEmptyString(body.request_id);\n\tif (requestId) {\n\t\tdetails.push(`request ${requestId}`);\n\t}\n\treturn details.length > 0 ? `${message} (${details.join(\", \")})` : message;\n}\n\nfunction asRecord(value: unknown): Record<string, unknown> | undefined {\n\treturn typeof value === \"object\" && value !== null && !Array.isArray(value)\n\t\t? (value as Record<string, unknown>)\n\t\t: undefined;\n}\n\nfunction asNonEmptyString(value: unknown): string | undefined {\n\treturn typeof value === \"string\" && value.length > 0 ? value : undefined;\n}\n\nfunction sanitizeTitle(text: string): string {\n\treturn text\n\t\t.replace(/\\u001b\\][^\\u0007]*(?:\\u0007|\\u001b\\\\)/g, \"\")\n\t\t.replace(/\\u001b\\[[0-?]*[ -/]*[@-~]/g, \"\")\n\t\t.replace(/\\u001b[\\u0020-\\u002f]*[\\u0030-\\u007e]/g, \"\")\n\t\t.replace(/[\\r\\n]+/g, \" \")\n\t\t.replace(/[\\u0000-\\u001f\\u007f-\\u009f]+/g, \" \")\n\t\t.replace(/\\s+/g, \" \")\n\t\t.replace(/^[\"'`]+|[\"'`.!?]+$/g, \"\")\n\t\t.trim()\n\t\t.slice(0, 80)\n\t\t.trim();\n}\n"]}
|
|
1
|
+
{"version":3,"file":"session-title-generator.js","sourceRoot":"","sources":["../../src/core/session-title-generator.ts"],"names":[],"mappings":"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","sourcesContent":["import type { StreamFn } from \"@earendil-works/pi-agent-core\";\nimport type {\n\tApi,\n\tAssistantMessage,\n\tContext,\n\tModel,\n\tSimpleStreamOptions,\n\tTextContent,\n\tThinkingLevel,\n} from \"@earendil-works/pi-ai/compat\";\nimport { completeSimple, type RetryPolicy, retryAssistantCall } from \"@earendil-works/pi-ai/compat\";\nimport { initialTitleReasoning, mandatoryReasoningRetryLevel } from \"./session-title-reasoning.ts\";\n\ninterface SessionTitleAuth {\n\treadonly apiKey?: string;\n\treadonly headers?: Record<string, string>;\n\treadonly extraBody?: Record<string, unknown>;\n\treadonly env?: Record<string, string>;\n}\n\ninterface GenerateSessionTitleOptions {\n\treadonly firstPrompt: string;\n\treadonly model: Model<Api>;\n\treadonly auth: SessionTitleAuth;\n\treadonly sessionId: string;\n\treadonly baseOptions?: SimpleStreamOptions;\n\treadonly signal?: AbortSignal;\n\treadonly streamFn?: StreamFn;\n\treadonly retry?: RetryPolicy;\n}\n\nconst TITLE_SYSTEM_PROMPT = `Generate a concise title for this coding-agent session.\n\nRules:\n- Use 3 to 6 words.\n- Prefer concrete nouns and verbs from the user's task.\n- Do not include quotes, punctuation at the end, markdown, or explanations.\n- If the input is only a greeting, acknowledgement, or too vague to title, return <title>none</title>.\n- Respond only as <title>Session Title</title>.`;\n\nconst LOW_SIGNAL_PROMPTS = new Set([\n\t\"hi\",\n\t\"hello\",\n\t\"hey\",\n\t\"yo\",\n\t\"thanks\",\n\t\"thank you\",\n\t\"ok\",\n\t\"okay\",\n\t\"k\",\n\t\"yes\",\n\t\"no\",\n\t\"yep\",\n\t\"nope\",\n]);\n\nconst TITLE_MAX_TOKENS = 64;\n// Reasoning tokens count against the completion budget, so a title that must reason needs room\n// for the thinking and the `<title>` answer.\nconst REASONING_TITLE_MAX_TOKENS = 1024;\n\nconst MAX_TITLE_RETRIES = 1;\nconst MAX_TITLE_RETRY_DELAY_MS = 2000;\n\n/**\n * Narrow the user's retry budget for cosmetic background title generation.\n *\n * A title is not worth the full agent-turn budget: the default policy would\n * keep hitting an already-overloaded provider for ~14s while the user's real\n * turn competes for the same capacity, and a custom `baseDelayMs` could stall\n * it far longer. One retry recovers the common single-blip case; a title that\n * still fails is regenerated at the next turn end anyway. `enabled` stays the\n * user's call, and a smaller configured budget is never inflated.\n */\nexport function sessionTitleRetryPolicy(settings: RetryPolicy): RetryPolicy {\n\treturn {\n\t\tenabled: settings.enabled,\n\t\tmaxRetries: Math.min(settings.maxRetries, MAX_TITLE_RETRIES),\n\t\tbaseDelayMs: Math.min(settings.baseDelayMs, MAX_TITLE_RETRY_DELAY_MS),\n\t};\n}\n\nexport function shouldSkipSessionTitle(text: string): boolean {\n\tconst normalized = text.replace(/\\s+/g, \" \").trim().toLowerCase();\n\tif (normalized.length === 0) return true;\n\tif (normalized.startsWith(\"/\")) return true;\n\treturn LOW_SIGNAL_PROMPTS.has(normalized);\n}\n\nexport async function generateSessionTitle(options: GenerateSessionTitleOptions): Promise<string | undefined> {\n\tif (shouldSkipSessionTitle(options.firstPrompt)) {\n\t\treturn undefined;\n\t}\n\n\t// Titles are cosmetic background work: honor the caller's retry policy so a\n\t// single transient provider error (e.g. a 529 overloaded stream) does not\n\t// surface as a scary runtime error. Mirrors completeSummarization().\n\tconst reasoning = initialTitleReasoning(options.model);\n\tlet response = await requestTitle(options, reasoning);\n\tconst retryReasoning =\n\t\tresponse.stopReason === \"error\"\n\t\t\t? mandatoryReasoningRetryLevel(options.model, reasoning, response.errorMessage)\n\t\t\t: undefined;\n\tif (retryReasoning !== undefined) {\n\t\tresponse = await requestTitle(options, retryReasoning);\n\t}\n\tif (response.stopReason === \"error\") {\n\t\tthrow new Error(humanizeProviderError(response.errorMessage ?? \"Session title generation failed\"));\n\t}\n\treturn parseSessionTitle(response);\n}\n\nfunction requestTitle(\n\toptions: GenerateSessionTitleOptions,\n\treasoning: ThinkingLevel | undefined,\n): Promise<AssistantMessage> {\n\treturn retryAssistantCall(\n\t\t() =>\n\t\t\tcompleteTitle(\n\t\t\t\toptions.model,\n\t\t\t\tbuildTitleContext(options.firstPrompt),\n\t\t\t\tbuildTitleOptions(options, reasoning),\n\t\t\t\toptions.streamFn,\n\t\t\t),\n\t\toptions.retry,\n\t\toptions.signal,\n\t);\n}\n\nfunction buildTitleContext(firstPrompt: string): Context {\n\treturn {\n\t\tsystemPrompt: TITLE_SYSTEM_PROMPT,\n\t\tmessages: [\n\t\t\t{\n\t\t\t\trole: \"user\",\n\t\t\t\tcontent: [{ type: \"text\", text: firstPrompt }],\n\t\t\t\ttimestamp: Date.now(),\n\t\t\t},\n\t\t],\n\t};\n}\n\nfunction buildTitleOptions(\n\toptions: GenerateSessionTitleOptions,\n\treasoning: ThinkingLevel | undefined,\n): SimpleStreamOptions {\n\tconst titleOptions: SimpleStreamOptions = {\n\t\t...options.baseOptions,\n\t\tsessionId: options.sessionId,\n\t\tcacheRetention: options.model.cacheRetention === \"none\" ? \"none\" : \"short\",\n\t\tmaxTokens: reasoning === undefined ? TITLE_MAX_TOKENS : REASONING_TITLE_MAX_TOKENS,\n\t};\n\tif (reasoning !== undefined) {\n\t\ttitleOptions.reasoning = reasoning;\n\t}\n\tif (options.auth.apiKey !== undefined) {\n\t\ttitleOptions.apiKey = options.auth.apiKey;\n\t}\n\tif (options.auth.headers !== undefined) {\n\t\ttitleOptions.headers = options.auth.headers;\n\t}\n\tif (options.auth.extraBody !== undefined) {\n\t\ttitleOptions.extraBody = options.auth.extraBody;\n\t}\n\tif (options.auth.env !== undefined) {\n\t\ttitleOptions.env = options.auth.env;\n\t}\n\tif (options.signal !== undefined) {\n\t\ttitleOptions.signal = options.signal;\n\t}\n\treturn titleOptions;\n}\n\nasync function completeTitle(\n\tmodel: Model<Api>,\n\tcontext: Context,\n\toptions: SimpleStreamOptions,\n\tstreamFn?: StreamFn,\n): Promise<AssistantMessage> {\n\tif (streamFn === undefined) {\n\t\treturn completeSimple(model, context, options);\n\t}\n\tconst stream = await streamFn(model, context, options);\n\treturn stream.result();\n}\n\nfunction parseSessionTitle(message: AssistantMessage): string | undefined {\n\tconst text = message.content\n\t\t.filter((part): part is TextContent => part.type === \"text\")\n\t\t.map((part) => part.text)\n\t\t.join(\"\")\n\t\t.trim();\n\tconst match = text.match(/<title>\\s*([\\s\\S]*?)\\s*<\\/title>/i);\n\tconst rawTitle = match?.[1];\n\tif (rawTitle === undefined) {\n\t\treturn undefined;\n\t}\n\tconst title = sanitizeTitle(rawTitle);\n\tif (!title || title.toLowerCase() === \"none\") {\n\t\treturn undefined;\n\t}\n\treturn title;\n}\n\n/**\n * Reduce a raw provider error payload (e.g. an Anthropic `{\"type\":\"error\",...}`\n * body, optionally prefixed with an HTTP status code) to a short human-readable\n * line while keeping the error type and request id for follow-up. Non-JSON and\n * unrecognized payloads pass through unchanged.\n */\nexport function humanizeProviderError(raw: string): string {\n\t// `formatProviderError` (packages/ai/src/utils/error-body.ts) emits\n\t// `<status>: <body>` or `<prefix> (<status>): <body>`; Anthropic SSE error\n\t// events arrive as the bare JSON body.\n\tconst statusPrefix = raw.trim().match(/^(?:.*?\\((\\d{3})\\)|(\\d{3}))\\s*:\\s*(?=\\{)/);\n\tconst status = statusPrefix?.[1] ?? statusPrefix?.[2];\n\tconst bodyText = statusPrefix ? raw.trim().slice(statusPrefix[0].length) : raw.trim();\n\tif (!bodyText.startsWith(\"{\")) {\n\t\treturn raw;\n\t}\n\tlet parsed: unknown;\n\ttry {\n\t\tparsed = JSON.parse(bodyText);\n\t} catch {\n\t\treturn raw;\n\t}\n\tconst body = asRecord(parsed);\n\tif (!body) {\n\t\treturn raw;\n\t}\n\tconst nested = asRecord(body.error);\n\tconst message = asNonEmptyString(nested?.message) ?? asNonEmptyString(body.message) ?? asNonEmptyString(body.error);\n\tif (message === undefined) {\n\t\treturn raw;\n\t}\n\tconst details: string[] = [];\n\tconst errorType = asNonEmptyString(nested?.type) ?? asNonEmptyString(body.type);\n\tif (errorType && errorType !== \"error\") {\n\t\tdetails.push(errorType);\n\t}\n\tif (status) {\n\t\tdetails.push(`HTTP ${status}`);\n\t}\n\tconst requestId = asNonEmptyString(body.request_id);\n\tif (requestId) {\n\t\tdetails.push(`request ${requestId}`);\n\t}\n\treturn details.length > 0 ? `${message} (${details.join(\", \")})` : message;\n}\n\nfunction asRecord(value: unknown): Record<string, unknown> | undefined {\n\treturn typeof value === \"object\" && value !== null && !Array.isArray(value)\n\t\t? (value as Record<string, unknown>)\n\t\t: undefined;\n}\n\nfunction asNonEmptyString(value: unknown): string | undefined {\n\treturn typeof value === \"string\" && value.length > 0 ? value : undefined;\n}\n\nfunction sanitizeTitle(text: string): string {\n\treturn text\n\t\t.replace(/\\u001b\\][^\\u0007]*(?:\\u0007|\\u001b\\\\)/g, \"\")\n\t\t.replace(/\\u001b\\[[0-?]*[ -/]*[@-~]/g, \"\")\n\t\t.replace(/\\u001b[\\u0020-\\u002f]*[\\u0030-\\u007e]/g, \"\")\n\t\t.replace(/[\\r\\n]+/g, \" \")\n\t\t.replace(/[\\u0000-\\u001f\\u007f-\\u009f]+/g, \" \")\n\t\t.replace(/\\s+/g, \" \")\n\t\t.replace(/^[\"'`]+|[\"'`.!?]+$/g, \"\")\n\t\t.trim()\n\t\t.slice(0, 80)\n\t\t.trim();\n}\n"]}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { Api, Model, ThinkingLevel } from "@earendil-works/pi-ai/compat";
|
|
2
|
+
/**
|
|
3
|
+
* Reasoning level for the first title request. Titles stay reasoning-free unless the catalog
|
|
4
|
+
* says the model cannot turn reasoning off; then the lowest supported level is requested, because
|
|
5
|
+
* an unset level maps to the provider's "disabled" value, which those endpoints reject with 400.
|
|
6
|
+
*/
|
|
7
|
+
export declare function initialTitleReasoning(model: Model<Api>): ThinkingLevel | undefined;
|
|
8
|
+
/**
|
|
9
|
+
* Reasoning level for one retry after a reasoning-free title request was rejected because the
|
|
10
|
+
* endpoint mandates reasoning, which means its catalog entry is stale. `low` is the effort every
|
|
11
|
+
* such endpoint seen so far accepts, clamped to what the catalog still allows.
|
|
12
|
+
*/
|
|
13
|
+
export declare function mandatoryReasoningRetryLevel(model: Model<Api>, sentReasoning: ThinkingLevel | undefined, errorMessage: string | undefined): ThinkingLevel | undefined;
|
|
14
|
+
//# sourceMappingURL=session-title-reasoning.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"session-title-reasoning.d.ts","sourceRoot":"","sources":["../../src/core/session-title-reasoning.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,GAAG,EAAE,KAAK,EAAsB,aAAa,EAAE,MAAM,8BAA8B,CAAC;AAKlG;;;;GAIG;AACH,wBAAgB,qBAAqB,CAAC,KAAK,EAAE,KAAK,CAAC,GAAG,CAAC,GAAG,aAAa,GAAG,SAAS,CAKlF;AAED;;;;GAIG;AACH,wBAAgB,4BAA4B,CAC3C,KAAK,EAAE,KAAK,CAAC,GAAG,CAAC,EACjB,aAAa,EAAE,aAAa,GAAG,SAAS,EACxC,YAAY,EAAE,MAAM,GAAG,SAAS,GAC9B,aAAa,GAAG,SAAS,CAI3B"}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { clampThinkingLevel, getSupportedThinkingLevels } from "@earendil-works/pi-ai/compat";
|
|
2
|
+
const REASONING_MANDATORY_ERROR = /reasoning is mandatory/i;
|
|
3
|
+
/**
|
|
4
|
+
* Reasoning level for the first title request. Titles stay reasoning-free unless the catalog
|
|
5
|
+
* says the model cannot turn reasoning off; then the lowest supported level is requested, because
|
|
6
|
+
* an unset level maps to the provider's "disabled" value, which those endpoints reject with 400.
|
|
7
|
+
*/
|
|
8
|
+
export function initialTitleReasoning(model) {
|
|
9
|
+
if (!model.reasoning)
|
|
10
|
+
return undefined;
|
|
11
|
+
const levels = getSupportedThinkingLevels(model);
|
|
12
|
+
if (levels.includes("off"))
|
|
13
|
+
return undefined;
|
|
14
|
+
return asReasoningLevel(levels[0]);
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Reasoning level for one retry after a reasoning-free title request was rejected because the
|
|
18
|
+
* endpoint mandates reasoning, which means its catalog entry is stale. `low` is the effort every
|
|
19
|
+
* such endpoint seen so far accepts, clamped to what the catalog still allows.
|
|
20
|
+
*/
|
|
21
|
+
export function mandatoryReasoningRetryLevel(model, sentReasoning, errorMessage) {
|
|
22
|
+
if (sentReasoning !== undefined || errorMessage === undefined)
|
|
23
|
+
return undefined;
|
|
24
|
+
if (!REASONING_MANDATORY_ERROR.test(errorMessage))
|
|
25
|
+
return undefined;
|
|
26
|
+
return asReasoningLevel(clampThinkingLevel(model, "low"));
|
|
27
|
+
}
|
|
28
|
+
function asReasoningLevel(level) {
|
|
29
|
+
return level === undefined || level === "off" ? undefined : level;
|
|
30
|
+
}
|
|
31
|
+
//# sourceMappingURL=session-title-reasoning.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"session-title-reasoning.js","sourceRoot":"","sources":["../../src/core/session-title-reasoning.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,kBAAkB,EAAE,0BAA0B,EAAE,MAAM,8BAA8B,CAAC;AAE9F,MAAM,yBAAyB,GAAG,yBAAyB,CAAC;AAE5D;;;;GAIG;AACH,MAAM,UAAU,qBAAqB,CAAC,KAAiB;IACtD,IAAI,CAAC,KAAK,CAAC,SAAS;QAAE,OAAO,SAAS,CAAC;IACvC,MAAM,MAAM,GAAG,0BAA0B,CAAC,KAAK,CAAC,CAAC;IACjD,IAAI,MAAM,CAAC,QAAQ,CAAC,KAAK,CAAC;QAAE,OAAO,SAAS,CAAC;IAC7C,OAAO,gBAAgB,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC;AACpC,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,4BAA4B,CAC3C,KAAiB,EACjB,aAAwC,EACxC,YAAgC;IAEhC,IAAI,aAAa,KAAK,SAAS,IAAI,YAAY,KAAK,SAAS;QAAE,OAAO,SAAS,CAAC;IAChF,IAAI,CAAC,yBAAyB,CAAC,IAAI,CAAC,YAAY,CAAC;QAAE,OAAO,SAAS,CAAC;IACpE,OAAO,gBAAgB,CAAC,kBAAkB,CAAC,KAAK,EAAE,KAAK,CAAC,CAAC,CAAC;AAC3D,CAAC;AAED,SAAS,gBAAgB,CAAC,KAAqC;IAC9D,OAAO,KAAK,KAAK,SAAS,IAAI,KAAK,KAAK,KAAK,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,KAAK,CAAC;AACnE,CAAC","sourcesContent":["import type { Api, Model, ModelThinkingLevel, ThinkingLevel } from \"@earendil-works/pi-ai/compat\";\nimport { clampThinkingLevel, getSupportedThinkingLevels } from \"@earendil-works/pi-ai/compat\";\n\nconst REASONING_MANDATORY_ERROR = /reasoning is mandatory/i;\n\n/**\n * Reasoning level for the first title request. Titles stay reasoning-free unless the catalog\n * says the model cannot turn reasoning off; then the lowest supported level is requested, because\n * an unset level maps to the provider's \"disabled\" value, which those endpoints reject with 400.\n */\nexport function initialTitleReasoning(model: Model<Api>): ThinkingLevel | undefined {\n\tif (!model.reasoning) return undefined;\n\tconst levels = getSupportedThinkingLevels(model);\n\tif (levels.includes(\"off\")) return undefined;\n\treturn asReasoningLevel(levels[0]);\n}\n\n/**\n * Reasoning level for one retry after a reasoning-free title request was rejected because the\n * endpoint mandates reasoning, which means its catalog entry is stale. `low` is the effort every\n * such endpoint seen so far accepts, clamped to what the catalog still allows.\n */\nexport function mandatoryReasoningRetryLevel(\n\tmodel: Model<Api>,\n\tsentReasoning: ThinkingLevel | undefined,\n\terrorMessage: string | undefined,\n): ThinkingLevel | undefined {\n\tif (sentReasoning !== undefined || errorMessage === undefined) return undefined;\n\tif (!REASONING_MANDATORY_ERROR.test(errorMessage)) return undefined;\n\treturn asReasoningLevel(clampThinkingLevel(model, \"low\"));\n}\n\nfunction asReasoningLevel(level: ModelThinkingLevel | undefined): ThinkingLevel | undefined {\n\treturn level === undefined || level === \"off\" ? undefined : level;\n}\n"]}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"path-utils.d.ts","sourceRoot":"","sources":["../../../src/core/tools/path-utils.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"path-utils.d.ts","sourceRoot":"","sources":["../../../src/core/tools/path-utils.ts"],"names":[],"mappings":"AAoCA,wBAAsB,UAAU,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAOnE;AAED,wBAAgB,UAAU,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAEnD;AAED;;;GAGG;AACH,wBAAgB,YAAY,CAAC,QAAQ,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,MAAM,CAElE;AAED,wBAAgB,eAAe,CAAC,QAAQ,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,MAAM,CAqCrE;AAED,wBAAsB,oBAAoB,CAAC,QAAQ,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAqCzF"}
|