pim-agent 0.0.1 → 0.9.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 +21 -0
- package/README.md +293 -7
- package/bin/pim.ts +151 -0
- package/package.json +82 -4
- package/packages/core/src/attachments/AttachmentStore.ts +99 -0
- package/packages/core/src/attachments/Attachments.ts +69 -0
- package/packages/core/src/extensions/CoreExtensions.ts +51 -0
- package/packages/core/src/extensions/apply-patch/coordinator.ts +53 -0
- package/packages/core/src/extensions/apply-patch/executor.ts +550 -0
- package/packages/core/src/extensions/apply-patch/index.ts +69 -0
- package/packages/core/src/extensions/apply-patch/matcher.ts +47 -0
- package/packages/core/src/extensions/apply-patch/model.ts +39 -0
- package/packages/core/src/extensions/apply-patch/parser.ts +379 -0
- package/packages/core/src/extensions/apply-patch/render.ts +216 -0
- package/packages/core/src/extensions/apply-patch/schema.ts +37 -0
- package/packages/core/src/extensions/apply-patch/types.ts +30 -0
- package/packages/core/src/extensions/bash/capture.ts +86 -0
- package/packages/core/src/extensions/bash/format.ts +110 -0
- package/packages/core/src/extensions/bash/image.ts +31 -0
- package/packages/core/src/extensions/bash/index.ts +88 -0
- package/packages/core/src/extensions/bash/render.ts +31 -0
- package/packages/core/src/extensions/bash/run.ts +221 -0
- package/packages/core/src/extensions/bash/schema.ts +64 -0
- package/packages/core/src/extensions/edit/edit.ts +366 -0
- package/packages/core/src/extensions/edit/index.ts +36 -0
- package/packages/core/src/extensions/edit/render.ts +16 -0
- package/packages/core/src/extensions/edit/schema.ts +37 -0
- package/packages/core/src/extensions/glob/glob.ts +34 -0
- package/packages/core/src/extensions/glob/index.ts +83 -0
- package/packages/core/src/extensions/glob/render.ts +72 -0
- package/packages/core/src/extensions/glob/schema.ts +66 -0
- package/packages/core/src/extensions/grep/grep.ts +256 -0
- package/packages/core/src/extensions/grep/index.ts +114 -0
- package/packages/core/src/extensions/grep/render.ts +271 -0
- package/packages/core/src/extensions/grep/schema.ts +92 -0
- package/packages/core/src/extensions/read/ImageMemory.ts +42 -0
- package/packages/core/src/extensions/read/image.ts +57 -0
- package/packages/core/src/extensions/read/index.ts +88 -0
- package/packages/core/src/extensions/read/read.ts +191 -0
- package/packages/core/src/extensions/read/render.ts +58 -0
- package/packages/core/src/extensions/read/schema.ts +49 -0
- package/packages/core/src/extensions/subagent/index.ts +33 -0
- package/packages/core/src/extensions/subagent/render.ts +127 -0
- package/packages/core/src/extensions/subagent/schema.ts +9 -0
- package/packages/core/src/extensions/subagent/subagent.ts +419 -0
- package/packages/core/src/extensions/system-prompt/index.ts +28 -0
- package/packages/core/src/extensions/system-prompt/prompt.ts +228 -0
- package/packages/core/src/extensions/todo/index.ts +123 -0
- package/packages/core/src/extensions/todo/render.ts +156 -0
- package/packages/core/src/extensions/todo/schema.ts +24 -0
- package/packages/core/src/extensions/todo/todo.ts +185 -0
- package/packages/core/src/extensions/web-fetch/JinaReaderClient.ts +187 -0
- package/packages/core/src/extensions/web-fetch/WebViewFetchClient.ts +183 -0
- package/packages/core/src/extensions/web-fetch/WebViewMarkdownSnapshot.ts +512 -0
- package/packages/core/src/extensions/web-fetch/fetch.ts +265 -0
- package/packages/core/src/extensions/web-fetch/image.ts +165 -0
- package/packages/core/src/extensions/web-fetch/index.ts +83 -0
- package/packages/core/src/extensions/web-fetch/render.ts +100 -0
- package/packages/core/src/extensions/web-fetch/schema.ts +46 -0
- package/packages/core/src/extensions/web-search/ExaMcpClient.ts +251 -0
- package/packages/core/src/extensions/web-search/SearchBreaker.ts +147 -0
- package/packages/core/src/extensions/web-search/SearchChain.ts +77 -0
- package/packages/core/src/extensions/web-search/index.ts +90 -0
- package/packages/core/src/extensions/web-search/providers/DuckDuckGoProvider.ts +195 -0
- package/packages/core/src/extensions/web-search/providers/ExaProvider.ts +54 -0
- package/packages/core/src/extensions/web-search/providers/FirecrawlProvider.ts +144 -0
- package/packages/core/src/extensions/web-search/providers/SearchProvider.ts +118 -0
- package/packages/core/src/extensions/web-search/render.ts +49 -0
- package/packages/core/src/extensions/web-search/schema.ts +29 -0
- package/packages/core/src/extensions/web-search/search.ts +23 -0
- package/packages/core/src/extensions/write/index.ts +58 -0
- package/packages/core/src/extensions/write/render.ts +16 -0
- package/packages/core/src/extensions/write/schema.ts +14 -0
- package/packages/core/src/extensions/write/write.ts +87 -0
- package/packages/core/src/picker/FilePickerSuggestionEngine.ts +14 -0
- package/packages/core/src/picker/InProcessFilePickerSuggestionEngine.ts +52 -0
- package/packages/core/src/picker/PickerItem.ts +6 -0
- package/packages/core/src/picker/PickerService.ts +110 -0
- package/packages/core/src/picker/RemoteFilePickerSuggestionEngine.ts +56 -0
- package/packages/core/src/picker/WorkerFilePickerSuggestionEngine.ts +268 -0
- package/packages/core/src/picker/catalog.ts +220 -0
- package/packages/core/src/picker/commandRanker.ts +17 -0
- package/packages/core/src/picker/filePickerWorker.ts +66 -0
- package/packages/core/src/picker/filePickerWorkerMessages.ts +39 -0
- package/packages/core/src/picker/ranker.ts +245 -0
- package/packages/core/src/picker/token.ts +70 -0
- package/packages/core/src/session/AgentRuntime.ts +85 -0
- package/packages/core/src/session/EventLog.ts +213 -0
- package/packages/core/src/session/MessageText.ts +21 -0
- package/packages/core/src/session/ReadCursors.ts +94 -0
- package/packages/core/src/session/SessionCache.ts +68 -0
- package/packages/core/src/session/SessionHost.ts +876 -0
- package/packages/core/src/session/SessionLease.ts +279 -0
- package/packages/core/src/session/SessionRegistry.ts +169 -0
- package/packages/core/src/session/WriteMark.ts +37 -0
- package/packages/core/src/shared/Cli.ts +25 -0
- package/packages/core/src/shared/DaemonUnit.ts +13 -0
- package/packages/core/src/shared/DiffLines.ts +304 -0
- package/packages/core/src/shared/DiffRenderer.ts +283 -0
- package/packages/core/src/shared/DiffView.ts +29 -0
- package/packages/core/src/shared/Directories.ts +61 -0
- package/packages/core/src/shared/EditMatcher.ts +767 -0
- package/packages/core/src/shared/Errors.ts +9 -0
- package/packages/core/src/shared/ExtensionToggles.ts +130 -0
- package/packages/core/src/shared/FileEnumerator.ts +361 -0
- package/packages/core/src/shared/FileScanner.ts +57 -0
- package/packages/core/src/shared/FileWatch.ts +107 -0
- package/packages/core/src/shared/Format.ts +74 -0
- package/packages/core/src/shared/Fs.ts +91 -0
- package/packages/core/src/shared/FsErrors.ts +67 -0
- package/packages/core/src/shared/FuzzyMatcher.ts +71 -0
- package/packages/core/src/shared/Git.ts +89 -0
- package/packages/core/src/shared/GlobExclusions.ts +13 -0
- package/packages/core/src/shared/Http.ts +14 -0
- package/packages/core/src/shared/ImageMime.ts +36 -0
- package/packages/core/src/shared/Images.ts +357 -0
- package/packages/core/src/shared/Json.ts +19 -0
- package/packages/core/src/shared/Languages.ts +95 -0
- package/packages/core/src/shared/Levenshtein.ts +30 -0
- package/packages/core/src/shared/Lines.ts +79 -0
- package/packages/core/src/shared/McpClient.ts +444 -0
- package/packages/core/src/shared/OutputBudget.ts +84 -0
- package/packages/core/src/shared/PatchSummary.ts +40 -0
- package/packages/core/src/shared/Paths.ts +80 -0
- package/packages/core/src/shared/PimSettings.ts +128 -0
- package/packages/core/src/shared/PimVersion.ts +93 -0
- package/packages/core/src/shared/Pool.ts +26 -0
- package/packages/core/src/shared/Proc.ts +30 -0
- package/packages/core/src/shared/RateLimiter.ts +50 -0
- package/packages/core/src/shared/Renderer.ts +371 -0
- package/packages/core/src/shared/SafePath.ts +17 -0
- package/packages/core/src/shared/SearchRender.ts +82 -0
- package/packages/core/src/shared/SendFile.ts +23 -0
- package/packages/core/src/shared/SpillCache.ts +78 -0
- package/packages/core/src/shared/SubagentLogs.ts +102 -0
- package/packages/core/src/shared/Supervisor.ts +460 -0
- package/packages/core/src/shared/Sweeper.ts +32 -0
- package/packages/core/src/shared/Tools.ts +801 -0
- package/packages/core/src/shared/Updater.ts +193 -0
- package/packages/core/src/view/AnsiPainter.ts +227 -0
- package/packages/core/src/view/BodyRenderer.ts +102 -0
- package/packages/core/src/view/DiffBlocks.ts +52 -0
- package/packages/core/src/view/DiffLayout.ts +69 -0
- package/packages/core/src/view/MarkdownPainter.ts +314 -0
- package/packages/core/src/view/Painting.ts +182 -0
- package/packages/core/src/view/ViewBlock.ts +124 -0
- package/packages/daemon/src/Daemon.ts +63 -0
- package/packages/daemon/src/DaemonInstall.ts +115 -0
- package/packages/daemon/src/Surfaces.ts +66 -0
- package/packages/daemon/src/TelegramSurface.ts +30 -0
- package/packages/daemon/src/WebSurface.ts +57 -0
- package/packages/daemon/src/index.ts +37 -0
- package/packages/protocol/src/Command.ts +92 -0
- package/packages/protocol/src/ImageRoute.ts +23 -0
- package/packages/protocol/src/Protocol.ts +6 -0
- package/packages/protocol/src/ServerEvent.ts +251 -0
- package/packages/server/src/AttachmentEndpoint.ts +184 -0
- package/packages/server/src/ClientConnection.ts +265 -0
- package/packages/server/src/ImageEndpoint.ts +36 -0
- package/packages/server/src/ProbeClient.ts +312 -0
- package/packages/server/src/Reloader.ts +93 -0
- package/packages/server/src/SendFileTool.ts +88 -0
- package/packages/server/src/SessionCatalogue.ts +202 -0
- package/packages/server/src/SessionProjection.ts +194 -0
- package/packages/server/src/SessionStream.ts +601 -0
- package/packages/server/src/StaticClient.ts +95 -0
- package/packages/server/src/WebOptions.ts +72 -0
- package/packages/server/src/WsGateway.ts +563 -0
- package/packages/server/src/probe.ts +146 -0
- package/packages/telegram/src/Bot.ts +229 -0
- package/packages/telegram/src/Commands.ts +768 -0
- package/packages/telegram/src/Config.ts +148 -0
- package/packages/telegram/src/Markdown.ts +235 -0
- package/packages/telegram/src/Message.ts +152 -0
- package/packages/telegram/src/Renderer.ts +456 -0
- package/packages/telegram/src/SendFileSchema.ts +18 -0
- package/packages/telegram/src/SendFileTool.ts +65 -0
- package/packages/telegram/src/Session.ts +252 -0
- package/packages/telegram/src/SessionRegistry.ts +126 -0
- package/packages/telegram/src/TaskScheduler.ts +296 -0
- package/packages/telegram/src/TaskSchema.ts +88 -0
- package/packages/telegram/src/TaskStore.ts +67 -0
- package/packages/telegram/src/TaskTool.ts +208 -0
- package/packages/telegram/src/TelegramHtml.ts +288 -0
- package/packages/telegram/src/TypingIndicator.ts +43 -0
- package/packages/telegram/src/UpdateConfirm.ts +61 -0
- package/packages/tui/src/extensions/_init/index.ts +144 -0
- package/packages/tui/src/extensions/command-picker/index.ts +43 -0
- package/packages/tui/src/extensions/file-picker/index.ts +156 -0
- package/packages/tui/src/extensions/footer/index.ts +117 -0
- package/packages/tui/src/extensions/footer/powerline.ts +87 -0
- package/packages/tui/src/extensions/footer/segments.ts +202 -0
- package/packages/tui/src/extensions/pim/index.ts +89 -0
- package/packages/tui/src/extensions/session-lease/index.ts +248 -0
- package/packages/tui/src/extensions/tps/index.ts +118 -0
- package/packages/tui/src/extensions/working-indicator/index.ts +75 -0
- package/packages/tui/src/extensions/wrapProvider.ts +18 -0
- package/packages/tui/src/themes/pim-dark.json +84 -0
- package/packages/tui/src/themes/pim-light.json +84 -0
- package/packages/tui/src/themes/themeCliArgs.ts +20 -0
- package/packages/web/dist/client/apple-touch-icon.png +0 -0
- package/packages/web/dist/client/assets/bash-LmRjUKlR.js +1 -0
- package/packages/web/dist/client/assets/c-DBVbJFpC.js +1 -0
- package/packages/web/dist/client/assets/clojure-CR7dvLJ4.js +1 -0
- package/packages/web/dist/client/assets/cmake-CgP6gB4W.js +1 -0
- package/packages/web/dist/client/assets/commit-mono-latin-300-italic-B48RJMeK.woff +0 -0
- package/packages/web/dist/client/assets/commit-mono-latin-300-italic-DBq5bJCE.woff2 +0 -0
- package/packages/web/dist/client/assets/commit-mono-latin-400-italic-BXinMwCi.woff +0 -0
- package/packages/web/dist/client/assets/commit-mono-latin-400-italic-DjSHLl2N.woff2 +0 -0
- package/packages/web/dist/client/assets/commit-mono-latin-400-normal-s0S3qwFW.woff +0 -0
- package/packages/web/dist/client/assets/commit-mono-latin-400-normal-wzhe4RuD.woff2 +0 -0
- package/packages/web/dist/client/assets/commit-mono-latin-600-normal-BuQDXT0M.woff +0 -0
- package/packages/web/dist/client/assets/commit-mono-latin-600-normal-SwgSWHaV.woff2 +0 -0
- package/packages/web/dist/client/assets/commit-mono-latin-700-normal-1-GU0IUE.woff +0 -0
- package/packages/web/dist/client/assets/commit-mono-latin-700-normal-DU8mrtj2.woff2 +0 -0
- package/packages/web/dist/client/assets/core-CGdQx9la.js +3 -0
- package/packages/web/dist/client/assets/cpp-N2d8M7Nc.js +1 -0
- package/packages/web/dist/client/assets/csharp-yYt0Ikks.js +1 -0
- package/packages/web/dist/client/assets/css-D8M8GYaw.js +1 -0
- package/packages/web/dist/client/assets/diff-BhdZizse.js +1 -0
- package/packages/web/dist/client/assets/dockerfile-C0zufGsU.js +1 -0
- package/packages/web/dist/client/assets/elixir-DXIHlGsR.js +1 -0
- package/packages/web/dist/client/assets/erlang-BjYI8dhz.js +1 -0
- package/packages/web/dist/client/assets/go-CYstVVJV.js +1 -0
- package/packages/web/dist/client/assets/graphql-BHHhacaR.js +1 -0
- package/packages/web/dist/client/assets/haskell-DUkhXIda.js +1 -0
- package/packages/web/dist/client/assets/index-BMNwleWO.js +40 -0
- package/packages/web/dist/client/assets/index-CqjB2RPr.css +1 -0
- package/packages/web/dist/client/assets/ini-B1CroMsf.js +1 -0
- package/packages/web/dist/client/assets/java-Dde-Wnb4.js +1 -0
- package/packages/web/dist/client/assets/javascript-8AmHuH5O.js +1 -0
- package/packages/web/dist/client/assets/json-PmyKWHmm.js +1 -0
- package/packages/web/dist/client/assets/kotlin-DgSK8bu-.js +2 -0
- package/packages/web/dist/client/assets/less-Bp0mgmpG.js +1 -0
- package/packages/web/dist/client/assets/lua-1dXYaL-H.js +1 -0
- package/packages/web/dist/client/assets/makefile-BX-o3EIz.js +1 -0
- package/packages/web/dist/client/assets/markdown-D6FrnlqJ.js +1 -0
- package/packages/web/dist/client/assets/ocaml-B-P1Uyq8.js +1 -0
- package/packages/web/dist/client/assets/perl-CvqMZYuf.js +1 -0
- package/packages/web/dist/client/assets/php-DuZl5e-L.js +2 -0
- package/packages/web/dist/client/assets/powershell-BUe16zE4.js +1 -0
- package/packages/web/dist/client/assets/protobuf-DGOW1qT8.js +1 -0
- package/packages/web/dist/client/assets/python-gHmu6VPG.js +1 -0
- package/packages/web/dist/client/assets/r-DNWPCnjT.js +1 -0
- package/packages/web/dist/client/assets/ruby-BGpZ3p4_.js +1 -0
- package/packages/web/dist/client/assets/rust-BIKPVoi6.js +1 -0
- package/packages/web/dist/client/assets/scala-C9lP4FWY.js +1 -0
- package/packages/web/dist/client/assets/scss-gOyOKiXd.js +1 -0
- package/packages/web/dist/client/assets/sql-DWRKyvJ-.js +1 -0
- package/packages/web/dist/client/assets/swift-BTn9__hL.js +1 -0
- package/packages/web/dist/client/assets/typescript-CxkPLmSc.js +1 -0
- package/packages/web/dist/client/assets/vim-Dhhcn_CA.js +1 -0
- package/packages/web/dist/client/assets/xml-COl5yFSC.js +1 -0
- package/packages/web/dist/client/assets/yaml-DjOhhVLG.js +1 -0
- package/packages/web/dist/client/favicon.svg +1 -0
- package/packages/web/dist/client/icon-192.png +0 -0
- package/packages/web/dist/client/icon-512.png +0 -0
- package/packages/web/dist/client/icon-maskable.png +0 -0
- package/packages/web/dist/client/index.html +24 -0
- package/packages/web/dist/client/manifest.webmanifest +21 -0
- package/packages/web/dist/client/wordmark.svg +1 -0
|
@@ -0,0 +1,801 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
AgentToolResult,
|
|
3
|
+
ExtensionAPI,
|
|
4
|
+
ToolDefinition,
|
|
5
|
+
} from "@earendil-works/pi-coding-agent";
|
|
6
|
+
import { validateToolArguments } from "@earendil-works/pi-ai";
|
|
7
|
+
import type { Static, TSchema } from "typebox";
|
|
8
|
+
import { Levenshtein } from "./Levenshtein";
|
|
9
|
+
import { Renderer } from "./Renderer";
|
|
10
|
+
import { AnsiPainter } from "../view/AnsiPainter";
|
|
11
|
+
import { BodyRenderer } from "../view/BodyRenderer";
|
|
12
|
+
import type { ToolView } from "../view/ViewBlock";
|
|
13
|
+
|
|
14
|
+
export type ToolViewInput<TParams extends TSchema, TDetails> = {
|
|
15
|
+
/** Partially streamed while the call is in flight; treat fields as optional. */
|
|
16
|
+
readonly args: Static<TParams>;
|
|
17
|
+
/** A streaming snapshot rather than the final result while `isPartial`. */
|
|
18
|
+
readonly result?: AgentToolResult<TDetails>;
|
|
19
|
+
readonly isPartial: boolean;
|
|
20
|
+
readonly cwd: string;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
/** What a call to this tool can do to the host machine; declaring nothing means `unbounded`. */
|
|
24
|
+
export type ToolEffect<TParams extends TSchema = TSchema> =
|
|
25
|
+
| { readonly kind: "readOnly" }
|
|
26
|
+
| {
|
|
27
|
+
readonly kind: "writesPaths";
|
|
28
|
+
/** Paths as the model wrote them; throwing means "unknown" and is treated as `unbounded`. */
|
|
29
|
+
readonly paths: (args: Static<TParams>) => readonly string[];
|
|
30
|
+
}
|
|
31
|
+
| { readonly kind: "unbounded" };
|
|
32
|
+
|
|
33
|
+
/** `ToolEffect` with its parameter type erased, for name-keyed lookup. */
|
|
34
|
+
export type ErasedToolEffect = ToolEffect<TSchema> & {
|
|
35
|
+
readonly paths?: (args: unknown) => readonly string[];
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
/** Pi's tool definition plus pim's view model, which must be pure over `(args, result, cwd)` to replay. */
|
|
39
|
+
export type PimToolDefinition<
|
|
40
|
+
TParams extends TSchema,
|
|
41
|
+
TDetails = unknown,
|
|
42
|
+
TState = unknown,
|
|
43
|
+
> = ToolDefinition<TParams, TDetails, TState> & {
|
|
44
|
+
readonly toViewModel?: (input: ToolViewInput<TParams, TDetails>) => ToolView;
|
|
45
|
+
/** Omitted means `unbounded`. */
|
|
46
|
+
readonly effect?: ToolEffect<TParams>;
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
/** A registered `toViewModel` with its parameter types erased, for name-keyed lookup. */
|
|
50
|
+
export type ToolViewFactory = (input: {
|
|
51
|
+
readonly args: unknown;
|
|
52
|
+
readonly result?: AgentToolResult<unknown>;
|
|
53
|
+
readonly isPartial: boolean;
|
|
54
|
+
readonly cwd: string;
|
|
55
|
+
}) => ToolView;
|
|
56
|
+
|
|
57
|
+
type ViewRenderState<TDetails> = {
|
|
58
|
+
viewResult?: AgentToolResult<TDetails>;
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
type Issue = { readonly path: string; readonly message: string };
|
|
62
|
+
|
|
63
|
+
type JsonSchema = {
|
|
64
|
+
readonly type?: string;
|
|
65
|
+
readonly const?: unknown;
|
|
66
|
+
readonly properties?: Readonly<Record<string, JsonSchema>>;
|
|
67
|
+
readonly items?: JsonSchema | readonly JsonSchema[];
|
|
68
|
+
readonly anyOf?: readonly JsonSchema[];
|
|
69
|
+
readonly oneOf?: readonly JsonSchema[];
|
|
70
|
+
readonly enum?: readonly unknown[];
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
const viewFactories = new Map<string, ToolViewFactory>();
|
|
74
|
+
const effects = new Map<string, ErasedToolEffect>();
|
|
75
|
+
|
|
76
|
+
/** What a registered tool declared it can do; absent reads as `unbounded`. */
|
|
77
|
+
function effectOf(toolName: string): ErasedToolEffect | undefined {
|
|
78
|
+
return effects.get(toolName);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function baseView(
|
|
82
|
+
toolName: string,
|
|
83
|
+
call: {
|
|
84
|
+
readonly args: unknown;
|
|
85
|
+
readonly result?: AgentToolResult<unknown>;
|
|
86
|
+
readonly isPartial: boolean;
|
|
87
|
+
readonly cwd: string;
|
|
88
|
+
}
|
|
89
|
+
): ToolView {
|
|
90
|
+
return (
|
|
91
|
+
viewFactories.get(toolName)?.(call) ?? genericView(toolName, call.args)
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const branchesOf = (schema?: JsonSchema): readonly JsonSchema[] | undefined =>
|
|
96
|
+
schema?.anyOf ?? schema?.oneOf;
|
|
97
|
+
|
|
98
|
+
/** The view for one call, with a generic fallback for a tool that registered none. */
|
|
99
|
+
function viewOf(input: {
|
|
100
|
+
readonly name: string;
|
|
101
|
+
readonly args: unknown;
|
|
102
|
+
readonly result?: AgentToolResult<unknown>;
|
|
103
|
+
readonly isError?: boolean;
|
|
104
|
+
readonly isPartial: boolean;
|
|
105
|
+
readonly cwd: string;
|
|
106
|
+
}): ToolView {
|
|
107
|
+
const { name, result, isError, ...rest } = input;
|
|
108
|
+
if (isError === true) {
|
|
109
|
+
return errorView(name, rest, result);
|
|
110
|
+
}
|
|
111
|
+
return baseView(name, {
|
|
112
|
+
...rest,
|
|
113
|
+
...(result === undefined ? {} : { result }),
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function errorView(
|
|
118
|
+
name: string,
|
|
119
|
+
call: {
|
|
120
|
+
readonly args: unknown;
|
|
121
|
+
readonly isPartial: boolean;
|
|
122
|
+
readonly cwd: string;
|
|
123
|
+
},
|
|
124
|
+
result: AgentToolResult<unknown> | undefined
|
|
125
|
+
): ToolView {
|
|
126
|
+
const view = baseView(name, call);
|
|
127
|
+
const text =
|
|
128
|
+
result === undefined ? "" : Renderer.extractErrorText(result, "");
|
|
129
|
+
if (text === "") {
|
|
130
|
+
return view;
|
|
131
|
+
}
|
|
132
|
+
return {
|
|
133
|
+
...view,
|
|
134
|
+
body: [{ kind: "notice", severity: "error", text }],
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/** Wrap a tool definition so pi's validator errors are rewritten before reaching the model. */
|
|
139
|
+
function wrap<TParams extends TSchema, TDetails = unknown, TState = unknown>(
|
|
140
|
+
def: PimToolDefinition<TParams, TDetails, TState>
|
|
141
|
+
): ToolDefinition<TParams, TDetails, TState> {
|
|
142
|
+
const schema = def.parameters as unknown as JsonSchema;
|
|
143
|
+
// Pi rejects unknown definition fields, so strip pim-only ones here.
|
|
144
|
+
const { toViewModel, effect, ...piDef } = def;
|
|
145
|
+
if (toViewModel !== undefined) {
|
|
146
|
+
viewFactories.set(def.name, toViewModel as ToolViewFactory);
|
|
147
|
+
}
|
|
148
|
+
if (effect !== undefined) {
|
|
149
|
+
effects.set(def.name, effect as ErasedToolEffect);
|
|
150
|
+
}
|
|
151
|
+
return {
|
|
152
|
+
...piDef,
|
|
153
|
+
...(toViewModel === undefined ? {} : synthesizeRenderers(def, toViewModel)),
|
|
154
|
+
prepareArguments: (rawArgs: unknown): Static<TParams> => {
|
|
155
|
+
const prepared = def.prepareArguments
|
|
156
|
+
? def.prepareArguments(rawArgs)
|
|
157
|
+
: (rawArgs as Static<TParams>);
|
|
158
|
+
const cleaned = coerceQuotedEnums(prepared, schema) as Static<TParams>;
|
|
159
|
+
const strictIssues = checkStrictTypes(cleaned, schema, "");
|
|
160
|
+
if (strictIssues.length > 0) {
|
|
161
|
+
const lines = strictIssues.map((s) => ` - ${s}`).join("\n");
|
|
162
|
+
throw new Error(`Validation failed for tool "${def.name}":\n${lines}`);
|
|
163
|
+
}
|
|
164
|
+
let validated: Static<TParams>;
|
|
165
|
+
try {
|
|
166
|
+
validated = validateToolArguments(
|
|
167
|
+
{ name: def.name, parameters: def.parameters } as never,
|
|
168
|
+
{
|
|
169
|
+
type: "toolCall",
|
|
170
|
+
id: "",
|
|
171
|
+
name: def.name,
|
|
172
|
+
arguments: cleaned as Record<string, unknown>,
|
|
173
|
+
}
|
|
174
|
+
) as Static<TParams>;
|
|
175
|
+
} catch (err) {
|
|
176
|
+
throw new Error(rewriteValidationError(def.name, schema, err, cleaned));
|
|
177
|
+
}
|
|
178
|
+
const unknownKeys = findUnknownTopLevelKeys(schema, validated);
|
|
179
|
+
if (unknownKeys.length > 0) {
|
|
180
|
+
throw new Error(formatUnknownKeysError(def.name, schema, unknownKeys));
|
|
181
|
+
}
|
|
182
|
+
return validated;
|
|
183
|
+
},
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function register<
|
|
188
|
+
TParams extends TSchema,
|
|
189
|
+
TDetails = unknown,
|
|
190
|
+
TState = unknown,
|
|
191
|
+
>(pi: ExtensionAPI, def: PimToolDefinition<TParams, TDetails, TState>): void {
|
|
192
|
+
pi.registerTool(wrap(def));
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/** Rewrite a `validateToolArguments` error string into a clearer form. */
|
|
196
|
+
function rewriteValidationError(
|
|
197
|
+
toolName: string,
|
|
198
|
+
schema: JsonSchema,
|
|
199
|
+
err: unknown,
|
|
200
|
+
args?: unknown
|
|
201
|
+
): string {
|
|
202
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
203
|
+
if (!message.startsWith("Validation failed for tool")) {
|
|
204
|
+
return message;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
const raw = parseIssues(message);
|
|
208
|
+
const collapsed = collapseAnyOf(raw, schema, args);
|
|
209
|
+
const issues = collapsed.map((issue) => formatIssue(issue, schema));
|
|
210
|
+
|
|
211
|
+
const header = `Validation failed for tool "${toolName}":`;
|
|
212
|
+
if (issues.length === 0) {
|
|
213
|
+
return header;
|
|
214
|
+
}
|
|
215
|
+
return `${header}\n${issues.map((s) => ` - ${s}`).join("\n")}`;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
const GENERIC_ARG_KEYS = [
|
|
219
|
+
"path",
|
|
220
|
+
"command",
|
|
221
|
+
"query",
|
|
222
|
+
"pattern",
|
|
223
|
+
"url",
|
|
224
|
+
] as const;
|
|
225
|
+
|
|
226
|
+
function genericView(toolName: string, args: unknown): ToolView {
|
|
227
|
+
const record =
|
|
228
|
+
args && typeof args === "object" ? (args as Record<string, unknown>) : {};
|
|
229
|
+
const subject = GENERIC_ARG_KEYS.map((key) => record[key]).find(
|
|
230
|
+
(value): value is string => typeof value === "string" && value !== ""
|
|
231
|
+
);
|
|
232
|
+
return {
|
|
233
|
+
title: [
|
|
234
|
+
{
|
|
235
|
+
kind: "spans",
|
|
236
|
+
spans: [{ text: subject ? `${toolName} ${subject}` : toolName }],
|
|
237
|
+
},
|
|
238
|
+
],
|
|
239
|
+
};
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
function synthesizeRenderers<TParams extends TSchema, TDetails, TState>(
|
|
243
|
+
def: PimToolDefinition<TParams, TDetails, TState>,
|
|
244
|
+
toViewModel: (input: ToolViewInput<TParams, TDetails>) => ToolView
|
|
245
|
+
): Pick<
|
|
246
|
+
ToolDefinition<TParams, TDetails, TState>,
|
|
247
|
+
"renderCall" | "renderResult"
|
|
248
|
+
> {
|
|
249
|
+
return {
|
|
250
|
+
renderCall:
|
|
251
|
+
def.renderCall ??
|
|
252
|
+
((args, theme, context) => {
|
|
253
|
+
const state = context.state as ViewRenderState<TDetails>;
|
|
254
|
+
const view = toViewModel({
|
|
255
|
+
args,
|
|
256
|
+
result: state.viewResult,
|
|
257
|
+
isPartial: Boolean(context.isPartial),
|
|
258
|
+
cwd: context.cwd,
|
|
259
|
+
});
|
|
260
|
+
const title = AnsiPainter.paintTitle(view.title, theme);
|
|
261
|
+
return Renderer.renderToolCallTitle({
|
|
262
|
+
label: view.label ?? def.label,
|
|
263
|
+
title: title.text,
|
|
264
|
+
markdown: title.markdown,
|
|
265
|
+
labelColor: AnsiPainter.themeColorFor(view.labelTone),
|
|
266
|
+
theme,
|
|
267
|
+
context,
|
|
268
|
+
});
|
|
269
|
+
}),
|
|
270
|
+
renderResult:
|
|
271
|
+
def.renderResult ??
|
|
272
|
+
((result, options, theme, context) => {
|
|
273
|
+
const state = context.state as ViewRenderState<TDetails>;
|
|
274
|
+
// Defer the redraw: invalidating re-entrantly makes pi paint the body twice.
|
|
275
|
+
if (!options.isPartial && state.viewResult === undefined) {
|
|
276
|
+
state.viewResult = result;
|
|
277
|
+
queueMicrotask(() => context.invalidate());
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
if (context.isError) {
|
|
281
|
+
return Renderer.renderErrorResult({
|
|
282
|
+
result,
|
|
283
|
+
options,
|
|
284
|
+
theme,
|
|
285
|
+
context,
|
|
286
|
+
});
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
const view = toViewModel({
|
|
290
|
+
args: context.args,
|
|
291
|
+
result,
|
|
292
|
+
isPartial: options.isPartial,
|
|
293
|
+
cwd: context.cwd,
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
return BodyRenderer.render({
|
|
297
|
+
summary: view.summary,
|
|
298
|
+
body: view.body,
|
|
299
|
+
options,
|
|
300
|
+
theme,
|
|
301
|
+
context,
|
|
302
|
+
});
|
|
303
|
+
}),
|
|
304
|
+
};
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
function parseIssues(message: string): Issue[] {
|
|
308
|
+
const issues: Issue[] = [];
|
|
309
|
+
for (const line of message.split("\n")) {
|
|
310
|
+
if (line.startsWith("Received arguments:")) {
|
|
311
|
+
break;
|
|
312
|
+
}
|
|
313
|
+
if (!line.startsWith(" - ")) {
|
|
314
|
+
continue;
|
|
315
|
+
}
|
|
316
|
+
const body = line.slice(4);
|
|
317
|
+
const colonIdx = body.indexOf(": ");
|
|
318
|
+
if (colonIdx === -1) {
|
|
319
|
+
issues.push({ path: "", message: body });
|
|
320
|
+
} else {
|
|
321
|
+
issues.push({
|
|
322
|
+
path: body.slice(0, colonIdx),
|
|
323
|
+
message: body.slice(colonIdx + 2),
|
|
324
|
+
});
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
return issues;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
function collapseAnyOf(
|
|
331
|
+
issues: readonly Issue[],
|
|
332
|
+
schema: JsonSchema,
|
|
333
|
+
args: unknown
|
|
334
|
+
): Issue[] {
|
|
335
|
+
const handled = new Set<number>();
|
|
336
|
+
const inserts = new Map<number, Issue[]>();
|
|
337
|
+
|
|
338
|
+
issues.forEach((issue, idx) => {
|
|
339
|
+
if (handled.has(idx) || issue.message !== "must match a schema in anyOf") {
|
|
340
|
+
return;
|
|
341
|
+
}
|
|
342
|
+
const node = walkSchema(schema, issue.path);
|
|
343
|
+
const branches = branchesOf(node);
|
|
344
|
+
if (!node || !branches) {
|
|
345
|
+
return;
|
|
346
|
+
}
|
|
347
|
+
handled.add(idx);
|
|
348
|
+
issues.forEach((other, otherIdx) => {
|
|
349
|
+
if (handled.has(otherIdx)) {
|
|
350
|
+
return;
|
|
351
|
+
}
|
|
352
|
+
if (other.path === issue.path || isUnderPath(other.path, issue.path)) {
|
|
353
|
+
handled.add(otherIdx);
|
|
354
|
+
}
|
|
355
|
+
});
|
|
356
|
+
|
|
357
|
+
const value = walkValue(args, issue.path);
|
|
358
|
+
const matched = matchDiscriminatedBranch(branches, value);
|
|
359
|
+
if (matched) {
|
|
360
|
+
const branchIssues = revalidateBranch(matched, value).map((sub) => ({
|
|
361
|
+
path: joinPath(issue.path, sub.path),
|
|
362
|
+
message: sub.message,
|
|
363
|
+
}));
|
|
364
|
+
inserts.set(idx, branchIssues);
|
|
365
|
+
} else {
|
|
366
|
+
inserts.set(idx, [
|
|
367
|
+
{ path: issue.path, message: describeAnyOf(branches) },
|
|
368
|
+
]);
|
|
369
|
+
}
|
|
370
|
+
});
|
|
371
|
+
|
|
372
|
+
const result: Issue[] = [];
|
|
373
|
+
issues.forEach((issue, idx) => {
|
|
374
|
+
if (inserts.has(idx)) {
|
|
375
|
+
result.push(...inserts.get(idx)!);
|
|
376
|
+
return;
|
|
377
|
+
}
|
|
378
|
+
if (!handled.has(idx)) {
|
|
379
|
+
result.push(issue);
|
|
380
|
+
}
|
|
381
|
+
});
|
|
382
|
+
return result;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
function describeAnyOf(branches: readonly JsonSchema[]): string {
|
|
386
|
+
const constValues = branches
|
|
387
|
+
.map((b) => (b && "const" in b ? b.const : undefined))
|
|
388
|
+
.filter((v) => v !== undefined);
|
|
389
|
+
if (constValues.length === branches.length) {
|
|
390
|
+
return `must be one of: ${constValues.map(displayValue).join(", ")}`;
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
const discriminator = findDiscriminator(branches);
|
|
394
|
+
if (discriminator) {
|
|
395
|
+
const values = discriminator.values.map(displayValue).join(", ");
|
|
396
|
+
return `must match one of the allowed variants (${discriminator.field}: ${values})`;
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
return `must match one of ${branches.length} allowed variants`;
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
function displayValue(value: unknown): string {
|
|
403
|
+
return typeof value === "string" ? value : JSON.stringify(value);
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
function matchDiscriminatedBranch(
|
|
407
|
+
branches: readonly JsonSchema[],
|
|
408
|
+
value: unknown
|
|
409
|
+
): JsonSchema | undefined {
|
|
410
|
+
if (!isRecord(value)) {
|
|
411
|
+
return undefined;
|
|
412
|
+
}
|
|
413
|
+
const discriminator = findDiscriminator(branches);
|
|
414
|
+
if (!discriminator) {
|
|
415
|
+
return undefined;
|
|
416
|
+
}
|
|
417
|
+
const actual = value[discriminator.field];
|
|
418
|
+
const branchIndex = discriminator.values.findIndex(
|
|
419
|
+
(v) => JSON.stringify(v) === JSON.stringify(actual)
|
|
420
|
+
);
|
|
421
|
+
return branchIndex >= 0 ? branches[branchIndex] : undefined;
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
function findDiscriminator(
|
|
425
|
+
branches: readonly JsonSchema[]
|
|
426
|
+
): { readonly field: string; readonly values: readonly unknown[] } | undefined {
|
|
427
|
+
const objectBranches = branches.filter(
|
|
428
|
+
(b) => b.type === "object" && b.properties
|
|
429
|
+
);
|
|
430
|
+
if (
|
|
431
|
+
objectBranches.length !== branches.length ||
|
|
432
|
+
objectBranches.length === 0
|
|
433
|
+
) {
|
|
434
|
+
return undefined;
|
|
435
|
+
}
|
|
436
|
+
for (const propName of Object.keys(objectBranches[0]!.properties!)) {
|
|
437
|
+
const values: unknown[] = [];
|
|
438
|
+
for (const branch of objectBranches) {
|
|
439
|
+
const prop = branch.properties![propName];
|
|
440
|
+
if (prop && "const" in prop) {
|
|
441
|
+
values.push(prop.const);
|
|
442
|
+
} else {
|
|
443
|
+
break;
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
if (
|
|
447
|
+
values.length === objectBranches.length &&
|
|
448
|
+
new Set(values.map((v) => JSON.stringify(v))).size === values.length
|
|
449
|
+
) {
|
|
450
|
+
return { field: propName, values };
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
return undefined;
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
function revalidateBranch(branch: JsonSchema, value: unknown): Issue[] {
|
|
457
|
+
try {
|
|
458
|
+
validateToolArguments(
|
|
459
|
+
{ name: "_branch", parameters: branch as TSchema } as never,
|
|
460
|
+
{
|
|
461
|
+
type: "toolCall",
|
|
462
|
+
id: "",
|
|
463
|
+
name: "_branch",
|
|
464
|
+
arguments: (value ?? {}) as Record<string, unknown>,
|
|
465
|
+
}
|
|
466
|
+
);
|
|
467
|
+
return [];
|
|
468
|
+
} catch (err) {
|
|
469
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
470
|
+
return parseIssues(message);
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
function walkSchema(
|
|
475
|
+
schema: JsonSchema | undefined,
|
|
476
|
+
path: string
|
|
477
|
+
): JsonSchema | undefined {
|
|
478
|
+
if (!schema) {
|
|
479
|
+
return undefined;
|
|
480
|
+
}
|
|
481
|
+
if (!path) {
|
|
482
|
+
return schema;
|
|
483
|
+
}
|
|
484
|
+
let current: JsonSchema | undefined = schema;
|
|
485
|
+
for (const part of path.split(".")) {
|
|
486
|
+
if (!current) {
|
|
487
|
+
return undefined;
|
|
488
|
+
}
|
|
489
|
+
if (current.properties && part in current.properties) {
|
|
490
|
+
current = current.properties[part];
|
|
491
|
+
continue;
|
|
492
|
+
}
|
|
493
|
+
if (current.items) {
|
|
494
|
+
current = Array.isArray(current.items)
|
|
495
|
+
? current.items[Number(part)]
|
|
496
|
+
: current.items;
|
|
497
|
+
continue;
|
|
498
|
+
}
|
|
499
|
+
return undefined;
|
|
500
|
+
}
|
|
501
|
+
return current;
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
function walkValue(value: unknown, path: string): unknown {
|
|
505
|
+
if (!path) {
|
|
506
|
+
return value;
|
|
507
|
+
}
|
|
508
|
+
let current = value;
|
|
509
|
+
for (const part of path.split(".")) {
|
|
510
|
+
if (current === null || current === undefined) {
|
|
511
|
+
return undefined;
|
|
512
|
+
}
|
|
513
|
+
if (Array.isArray(current)) {
|
|
514
|
+
current = current[Number(part)];
|
|
515
|
+
} else if (isRecord(current)) {
|
|
516
|
+
current = current[part];
|
|
517
|
+
} else {
|
|
518
|
+
return undefined;
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
return current;
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
function isUnderPath(candidate: string, parent: string): boolean {
|
|
525
|
+
if (!parent) {
|
|
526
|
+
return candidate.length > 0;
|
|
527
|
+
}
|
|
528
|
+
return candidate.startsWith(`${parent}.`);
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
function joinPath(parent: string, child: string): string {
|
|
532
|
+
if (!parent) {
|
|
533
|
+
return child;
|
|
534
|
+
}
|
|
535
|
+
if (!child) {
|
|
536
|
+
return parent;
|
|
537
|
+
}
|
|
538
|
+
return `${parent}.${child}`;
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
542
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
function formatIssue(issue: Issue, schema: JsonSchema): string {
|
|
546
|
+
const requiredMatch = issue.message.match(
|
|
547
|
+
/^must have required propert(?:y|ies) (.+)$/
|
|
548
|
+
);
|
|
549
|
+
if (requiredMatch) {
|
|
550
|
+
const props = requiredMatch[1]!;
|
|
551
|
+
const parent = issue.path.includes(".")
|
|
552
|
+
? issue.path.slice(0, issue.path.lastIndexOf("."))
|
|
553
|
+
: "";
|
|
554
|
+
const where = parent ? ` at ${parent}` : "";
|
|
555
|
+
const noun = props.includes(",") ? "properties" : "property";
|
|
556
|
+
return `missing required ${noun}${where}: ${props}`;
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
if (issue.message === "must be equal to one of the allowed values") {
|
|
560
|
+
const node = walkSchema(schema, issue.path);
|
|
561
|
+
if (node?.enum && node.enum.length > 0) {
|
|
562
|
+
const values = node.enum.map(displayValue).join(", ");
|
|
563
|
+
return `${issue.path}: must be one of: ${values}`;
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
if (!issue.path) {
|
|
568
|
+
return issue.message;
|
|
569
|
+
}
|
|
570
|
+
return `${issue.path}: ${issue.message}`;
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
function coerceQuotedEnums(
|
|
574
|
+
value: unknown,
|
|
575
|
+
schema: JsonSchema | undefined
|
|
576
|
+
): unknown {
|
|
577
|
+
if (!schema) {
|
|
578
|
+
return value;
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
if (typeof value === "string") {
|
|
582
|
+
const allowed = collectAllowedStrings(schema);
|
|
583
|
+
if (allowed && allowed.length > 0 && !allowed.includes(value)) {
|
|
584
|
+
const unwrapped = stripWrappingQuotes(value);
|
|
585
|
+
if (unwrapped !== value && allowed.includes(unwrapped)) {
|
|
586
|
+
return unwrapped;
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
return value;
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
if (isRecord(value)) {
|
|
593
|
+
let mutated: Record<string, unknown> | undefined;
|
|
594
|
+
const propSchemas = schema.properties;
|
|
595
|
+
for (const key of Object.keys(value)) {
|
|
596
|
+
const subSchema = propSchemas?.[key];
|
|
597
|
+
const next = coerceQuotedEnums(value[key], subSchema);
|
|
598
|
+
if (next !== value[key]) {
|
|
599
|
+
mutated ??= { ...value };
|
|
600
|
+
mutated[key] = next;
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
const branches = branchesOf(schema);
|
|
604
|
+
if (branches) {
|
|
605
|
+
const branch =
|
|
606
|
+
matchDiscriminatedBranch(branches, mutated ?? value) ??
|
|
607
|
+
branches.find((b) => b.type === "object" && b.properties);
|
|
608
|
+
if (branch) {
|
|
609
|
+
const recursed = coerceQuotedEnums(mutated ?? value, branch);
|
|
610
|
+
if (recursed !== (mutated ?? value)) {
|
|
611
|
+
return recursed;
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
return mutated ?? value;
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
if (Array.isArray(value)) {
|
|
619
|
+
const itemsField = schema.items;
|
|
620
|
+
if (!itemsField || Array.isArray(itemsField)) {
|
|
621
|
+
return value;
|
|
622
|
+
}
|
|
623
|
+
const itemSchema = itemsField as JsonSchema;
|
|
624
|
+
let mutated: unknown[] | undefined;
|
|
625
|
+
for (let i = 0; i < value.length; i++) {
|
|
626
|
+
const next = coerceQuotedEnums(value[i], itemSchema);
|
|
627
|
+
if (next !== value[i]) {
|
|
628
|
+
mutated ??= [...value];
|
|
629
|
+
mutated[i] = next;
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
return mutated ?? value;
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
return value;
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
function collectAllowedStrings(schema: JsonSchema): string[] | undefined {
|
|
639
|
+
if (schema.enum) {
|
|
640
|
+
const strings = schema.enum.filter(
|
|
641
|
+
(v): v is string => typeof v === "string"
|
|
642
|
+
);
|
|
643
|
+
return strings.length > 0 ? strings : undefined;
|
|
644
|
+
}
|
|
645
|
+
if (typeof schema.const === "string") {
|
|
646
|
+
return [schema.const];
|
|
647
|
+
}
|
|
648
|
+
const branches = branchesOf(schema);
|
|
649
|
+
if (branches) {
|
|
650
|
+
const collected: string[] = [];
|
|
651
|
+
for (const branch of branches) {
|
|
652
|
+
const inner = collectAllowedStrings(branch);
|
|
653
|
+
if (inner) {
|
|
654
|
+
collected.push(...inner);
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
return collected.length > 0 ? collected : undefined;
|
|
658
|
+
}
|
|
659
|
+
return undefined;
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
function stripWrappingQuotes(value: string): string {
|
|
663
|
+
if (value.length < 2) {
|
|
664
|
+
return value;
|
|
665
|
+
}
|
|
666
|
+
const first = value[0]!;
|
|
667
|
+
const last = value[value.length - 1]!;
|
|
668
|
+
const quoteChars = ['"', "'", "`"];
|
|
669
|
+
if (quoteChars.includes(first) && first === last) {
|
|
670
|
+
return value.slice(1, -1);
|
|
671
|
+
}
|
|
672
|
+
return value;
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
function checkStrictTypes(
|
|
676
|
+
value: unknown,
|
|
677
|
+
schema: JsonSchema | undefined,
|
|
678
|
+
path: string
|
|
679
|
+
): string[] {
|
|
680
|
+
if (!schema) {
|
|
681
|
+
return [];
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
const types = collectSchemaTypes(schema);
|
|
685
|
+
if (types.length > 0 && !types.includes("null") && value === null) {
|
|
686
|
+
return [
|
|
687
|
+
`${path || "root"}: must not be null (expected ${types.join(" | ")})`,
|
|
688
|
+
];
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
if (
|
|
692
|
+
types.includes("integer") &&
|
|
693
|
+
typeof value === "string" &&
|
|
694
|
+
/^-?\d+\.\d*[1-9]/.test(value)
|
|
695
|
+
) {
|
|
696
|
+
return [
|
|
697
|
+
`${path || "root"}: must be an integer (received "${value}" — fractional part would be truncated)`,
|
|
698
|
+
];
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
if (isRecord(value) && schema.properties) {
|
|
702
|
+
const issues: string[] = [];
|
|
703
|
+
for (const [key, sub] of Object.entries(value)) {
|
|
704
|
+
const subSchema = schema.properties[key];
|
|
705
|
+
if (subSchema) {
|
|
706
|
+
issues.push(...checkStrictTypes(sub, subSchema, joinPath(path, key)));
|
|
707
|
+
}
|
|
708
|
+
}
|
|
709
|
+
return issues;
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
if (Array.isArray(value)) {
|
|
713
|
+
const itemsField = schema.items;
|
|
714
|
+
if (!itemsField || Array.isArray(itemsField)) {
|
|
715
|
+
return [];
|
|
716
|
+
}
|
|
717
|
+
const itemSchema = itemsField as JsonSchema;
|
|
718
|
+
const issues: string[] = [];
|
|
719
|
+
for (let i = 0; i < value.length; i++) {
|
|
720
|
+
issues.push(
|
|
721
|
+
...checkStrictTypes(value[i], itemSchema, joinPath(path, String(i)))
|
|
722
|
+
);
|
|
723
|
+
}
|
|
724
|
+
return issues;
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
return [];
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
function collectSchemaTypes(schema: JsonSchema): string[] {
|
|
731
|
+
const types = new Set<string>();
|
|
732
|
+
if (typeof schema.type === "string") {
|
|
733
|
+
types.add(schema.type);
|
|
734
|
+
}
|
|
735
|
+
if (Array.isArray(schema.type)) {
|
|
736
|
+
for (const t of schema.type) {
|
|
737
|
+
if (typeof t === "string") {
|
|
738
|
+
types.add(t);
|
|
739
|
+
}
|
|
740
|
+
}
|
|
741
|
+
}
|
|
742
|
+
const branches = branchesOf(schema);
|
|
743
|
+
if (branches) {
|
|
744
|
+
for (const b of branches) {
|
|
745
|
+
for (const t of collectSchemaTypes(b)) {
|
|
746
|
+
types.add(t);
|
|
747
|
+
}
|
|
748
|
+
}
|
|
749
|
+
}
|
|
750
|
+
return Array.from(types);
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
function findUnknownTopLevelKeys(schema: JsonSchema, args: unknown): string[] {
|
|
754
|
+
if (schema.type !== "object" || !schema.properties || !isRecord(args)) {
|
|
755
|
+
return [];
|
|
756
|
+
}
|
|
757
|
+
const known = new Set(Object.keys(schema.properties));
|
|
758
|
+
return Object.keys(args).filter((key) => !known.has(key));
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
function formatUnknownKeysError(
|
|
762
|
+
toolName: string,
|
|
763
|
+
schema: JsonSchema,
|
|
764
|
+
unknownKeys: readonly string[]
|
|
765
|
+
): string {
|
|
766
|
+
const known = schema.properties ? Object.keys(schema.properties) : [];
|
|
767
|
+
const lines = unknownKeys.map((key) => {
|
|
768
|
+
const suggestion = closestKey(key, known);
|
|
769
|
+
const hint = suggestion ? ` (did you mean "${suggestion}"?)` : "";
|
|
770
|
+
return ` - unknown property: ${key}${hint}`;
|
|
771
|
+
});
|
|
772
|
+
return `Validation failed for tool "${toolName}":\n${lines.join("\n")}`;
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
function closestKey(
|
|
776
|
+
key: string,
|
|
777
|
+
candidates: readonly string[]
|
|
778
|
+
): string | undefined {
|
|
779
|
+
const lowered = key.toLowerCase();
|
|
780
|
+
let best: { key: string; distance: number } | undefined;
|
|
781
|
+
for (const candidate of candidates) {
|
|
782
|
+
if (candidate.toLowerCase() === lowered) {
|
|
783
|
+
return candidate;
|
|
784
|
+
}
|
|
785
|
+
const d = Levenshtein.distance(lowered, candidate.toLowerCase());
|
|
786
|
+
if (d <= Math.max(2, Math.floor(candidate.length / 3))) {
|
|
787
|
+
if (!best || d < best.distance) {
|
|
788
|
+
best = { key: candidate, distance: d };
|
|
789
|
+
}
|
|
790
|
+
}
|
|
791
|
+
}
|
|
792
|
+
return best?.key;
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
export const Tools = {
|
|
796
|
+
effectOf,
|
|
797
|
+
viewOf,
|
|
798
|
+
wrap,
|
|
799
|
+
register,
|
|
800
|
+
rewriteValidationError,
|
|
801
|
+
};
|