@oh-my-pi/pi-coding-agent 16.2.13 → 16.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +108 -7
- package/dist/cli.js +6033 -5982
- package/dist/types/advisor/config.d.ts +4 -2
- package/dist/types/collab/host.d.ts +16 -0
- package/dist/types/collab/protocol.d.ts +9 -3
- package/dist/types/commit/model-selection.d.ts +5 -0
- package/dist/types/config/model-resolver.d.ts +12 -10
- package/dist/types/config/settings-schema.d.ts +28 -5
- package/dist/types/edit/modes/patch.d.ts +11 -0
- package/dist/types/eval/agent-bridge.d.ts +5 -1
- package/dist/types/exec/bash-executor.d.ts +1 -0
- package/dist/types/extensibility/legacy-pi-coding-agent-shim.d.ts +86 -2
- package/dist/types/extensibility/skills.d.ts +2 -1
- package/dist/types/mnemopi/state.d.ts +12 -0
- package/dist/types/modes/components/agent-hub.d.ts +9 -5
- package/dist/types/modes/components/status-line/component.test.d.ts +1 -0
- package/dist/types/modes/components/usage-row.d.ts +1 -1
- package/dist/types/modes/controllers/command-controller.d.ts +0 -1
- package/dist/types/modes/controllers/extension-ui-controller.d.ts +6 -0
- package/dist/types/modes/controllers/streaming-reveal.d.ts +17 -1
- package/dist/types/modes/controllers/tool-args-reveal.d.ts +30 -3
- package/dist/types/modes/interactive-mode.d.ts +12 -7
- package/dist/types/modes/rpc/rpc-client.d.ts +5 -0
- package/dist/types/modes/rpc/rpc-mode.d.ts +64 -1
- package/dist/types/modes/session-teardown.d.ts +63 -0
- package/dist/types/modes/session-teardown.test.d.ts +1 -0
- package/dist/types/modes/theme/theme.d.ts +9 -4
- package/dist/types/modes/types.d.ts +2 -3
- package/dist/types/modes/utils/copy-targets.d.ts +2 -0
- package/dist/types/plan-mode/approved-plan-prompt.test.d.ts +1 -0
- package/dist/types/sdk.d.ts +9 -0
- package/dist/types/session/agent-session.d.ts +46 -13
- package/dist/types/session/exit-diagnostics.d.ts +48 -0
- package/dist/types/session/session-loader.d.ts +5 -0
- package/dist/types/task/executor.d.ts +12 -5
- package/dist/types/task/parallel.d.ts +1 -0
- package/dist/types/task/render.test.d.ts +1 -0
- package/dist/types/thinking.d.ts +2 -0
- package/dist/types/tools/checkpoint.d.ts +8 -0
- package/dist/types/tools/eval-render.d.ts +1 -0
- package/dist/types/tools/fetch.d.ts +11 -1
- package/dist/types/tools/index.d.ts +3 -1
- package/dist/types/tools/irc.d.ts +1 -0
- package/dist/types/tools/output-meta.d.ts +7 -0
- package/dist/types/tools/output-schema-validator.d.ts +7 -4
- package/dist/types/tools/path-utils.d.ts +16 -0
- package/dist/types/tools/render-utils.d.ts +4 -1
- package/dist/types/utils/git.d.ts +47 -2
- package/dist/types/web/search/provider.d.ts +7 -0
- package/dist/types/web/search/types.d.ts +1 -1
- package/package.json +12 -12
- package/src/advisor/config.ts +4 -2
- package/src/cli/bench-cli.ts +7 -2
- package/src/collab/guest.ts +94 -5
- package/src/collab/host.ts +80 -3
- package/src/collab/protocol.ts +9 -2
- package/src/commit/model-selection.ts +8 -2
- package/src/config/keybindings.ts +3 -1
- package/src/config/model-discovery.ts +66 -2
- package/src/config/model-registry.ts +7 -3
- package/src/config/model-resolver.ts +51 -23
- package/src/config/settings-schema.ts +44 -14
- package/src/edit/hashline/diff.ts +13 -2
- package/src/edit/index.ts +58 -8
- package/src/edit/modes/patch.ts +53 -18
- package/src/edit/streaming.ts +7 -6
- package/src/eval/__tests__/agent-bridge.test.ts +57 -1
- package/src/eval/agent-bridge.ts +15 -5
- package/src/exec/bash-executor.ts +7 -12
- package/src/extensibility/legacy-pi-coding-agent-shim.ts +534 -16
- package/src/extensibility/skills.ts +29 -6
- package/src/goals/guided-setup.ts +4 -3
- package/src/internal-urls/docs-index.generated.txt +1 -1
- package/src/lsp/client.ts +11 -14
- package/src/main.ts +38 -10
- package/src/mnemopi/state.ts +43 -7
- package/src/modes/acp/acp-agent.ts +1 -1
- package/src/modes/components/agent-hub.ts +10 -2
- package/src/modes/components/agent-transcript-viewer.ts +39 -7
- package/src/modes/components/assistant-message.ts +16 -10
- package/src/modes/components/chat-transcript-builder.ts +11 -1
- package/src/modes/components/custom-editor.test.ts +20 -0
- package/src/modes/components/hook-selector.ts +44 -25
- package/src/modes/components/model-selector.ts +1 -1
- package/src/modes/components/status-line/component.test.ts +44 -0
- package/src/modes/components/status-line/component.ts +9 -1
- package/src/modes/components/status-line/segments.ts +3 -3
- package/src/modes/components/usage-row.ts +16 -2
- package/src/modes/controllers/command-controller.ts +6 -21
- package/src/modes/controllers/event-controller.ts +11 -9
- package/src/modes/controllers/extension-ui-controller.ts +84 -3
- package/src/modes/controllers/input-controller.ts +16 -13
- package/src/modes/controllers/selector-controller.ts +3 -8
- package/src/modes/controllers/streaming-reveal.ts +75 -53
- package/src/modes/controllers/tool-args-reveal.ts +340 -10
- package/src/modes/interactive-mode.ts +122 -79
- package/src/modes/rpc/rpc-client.ts +21 -0
- package/src/modes/rpc/rpc-mode.ts +197 -46
- package/src/modes/session-teardown.test.ts +219 -0
- package/src/modes/session-teardown.ts +82 -0
- package/src/modes/setup-wizard/scenes/theme.ts +2 -2
- package/src/modes/skill-command.ts +7 -20
- package/src/modes/theme/theme.ts +29 -15
- package/src/modes/types.ts +2 -3
- package/src/modes/utils/copy-targets.ts +12 -0
- package/src/modes/utils/ui-helpers.ts +19 -2
- package/src/plan-mode/approved-plan-prompt.test.ts +36 -0
- package/src/prompts/advisor/system.md +1 -1
- package/src/prompts/agents/tester.md +6 -2
- package/src/prompts/skills/autoload.md +8 -0
- package/src/prompts/skills/user-invocation.md +11 -0
- package/src/prompts/steering/parent-irc.md +5 -0
- package/src/prompts/system/irc-incoming.md +2 -0
- package/src/prompts/system/mid-run-todo-nudge.md +3 -0
- package/src/prompts/system/plan-mode-approved.md +7 -10
- package/src/prompts/system/plan-mode-compact-instructions.md +2 -1
- package/src/prompts/system/plan-mode-reference.md +3 -4
- package/src/prompts/system/rewind-report.md +6 -0
- package/src/prompts/system/subagent-system-prompt.md +3 -0
- package/src/prompts/tools/irc.md +2 -1
- package/src/prompts/tools/job.md +2 -2
- package/src/prompts/tools/rewind.md +3 -2
- package/src/prompts/tools/task.md +4 -0
- package/src/sdk.ts +18 -4
- package/src/session/agent-session.ts +660 -114
- package/src/session/exit-diagnostics.ts +202 -0
- package/src/session/session-context.ts +25 -13
- package/src/session/session-loader.ts +58 -25
- package/src/session/session-manager.ts +0 -1
- package/src/session/session-persistence.ts +30 -4
- package/src/session/settings-stream-fn.ts +14 -0
- package/src/slash-commands/builtin-registry.ts +35 -3
- package/src/system-prompt.ts +15 -1
- package/src/task/executor.ts +31 -8
- package/src/task/index.ts +31 -9
- package/src/task/isolation-runner.ts +18 -2
- package/src/task/parallel.ts +7 -2
- package/src/task/render.test.ts +121 -0
- package/src/task/render.ts +48 -2
- package/src/task/worktree.ts +12 -13
- package/src/task/yield-assembly.ts +8 -5
- package/src/thinking.ts +5 -0
- package/src/tools/ask.ts +188 -9
- package/src/tools/ast-edit.ts +7 -0
- package/src/tools/ast-grep.ts +11 -0
- package/src/tools/bash.ts +6 -30
- package/src/tools/checkpoint.ts +15 -1
- package/src/tools/eval-render.ts +15 -0
- package/src/tools/fetch.ts +82 -18
- package/src/tools/gh.ts +1 -1
- package/src/tools/grep.ts +45 -27
- package/src/tools/index.ts +3 -1
- package/src/tools/irc.ts +24 -9
- package/src/tools/output-meta.ts +50 -0
- package/src/tools/output-schema-validator.ts +152 -15
- package/src/tools/path-utils.ts +55 -10
- package/src/tools/read.ts +1 -1
- package/src/tools/render-utils.ts +5 -3
- package/src/tools/yield.ts +41 -1
- package/src/utils/commit-message-generator.ts +2 -2
- package/src/utils/git.ts +271 -29
- package/src/utils/thinking-display.ts +13 -0
- package/src/web/search/index.ts +9 -28
- package/src/web/search/provider.ts +26 -1
- package/src/web/search/providers/duckduckgo.ts +1 -1
- package/src/web/search/types.ts +5 -1
|
@@ -8,11 +8,13 @@
|
|
|
8
8
|
* cannot be rejected post-mortem (or vice versa).
|
|
9
9
|
*/
|
|
10
10
|
import {
|
|
11
|
+
dereferenceJsonSchema,
|
|
11
12
|
isValidJsonSchema,
|
|
12
13
|
type JsonSchemaValidationIssue,
|
|
13
14
|
type JsonSchemaValidationResult,
|
|
14
15
|
validateJsonSchemaValue,
|
|
15
16
|
} from "@oh-my-pi/pi-ai/utils/schema";
|
|
17
|
+
import { isRecord } from "@oh-my-pi/pi-utils";
|
|
16
18
|
import { jtdToJsonSchema, normalizeSchema } from "./jtd-to-json-schema";
|
|
17
19
|
|
|
18
20
|
/** A validator bound to a specific output schema. */
|
|
@@ -25,12 +27,15 @@ export interface OutputValidator {
|
|
|
25
27
|
* Per-label validators for incremental yields (`type: ["<label>"]`). Each entry validates the
|
|
26
28
|
* `data` payload of a single section against the matching top-level property's sub-schema —
|
|
27
29
|
* array-typed properties (e.g. `findings`) use the items schema since each yield contributes
|
|
28
|
-
* one element, while scalar properties use the property schema directly.
|
|
29
|
-
* top-level properties) have no entry and skip per-call validation. Lets the yield tool give
|
|
30
|
-
* the model retry feedback on a section as soon as it arrives, instead of deferring every
|
|
31
|
-
* mismatch to the parent's post-mortem `schema_violation`.
|
|
30
|
+
* one element, while scalar properties use the property schema directly.
|
|
32
31
|
*/
|
|
33
32
|
readonly validateSection: ReadonlyMap<string, (value: unknown) => JsonSchemaValidationResult>;
|
|
33
|
+
/** Whether top-level schema closure makes unknown incremental yield labels invalid. */
|
|
34
|
+
readonly rejectUnknownSections: boolean;
|
|
35
|
+
/** Finite top-level section labels declared directly by the schema. Pattern-backed labels are accepted via `isKnownSection`. */
|
|
36
|
+
readonly knownSectionLabels: readonly string[];
|
|
37
|
+
/** Whether an incremental yield label is accepted by the top-level schema declaration. */
|
|
38
|
+
isKnownSection(label: string): boolean;
|
|
34
39
|
}
|
|
35
40
|
|
|
36
41
|
export interface BuildOutputValidatorResult {
|
|
@@ -75,14 +80,28 @@ export function buildOutputValidator(schema: unknown): BuildOutputValidatorResul
|
|
|
75
80
|
if (!isValidJsonSchema(jsonSchema)) return { error: "invalid JSON schema", normalized };
|
|
76
81
|
|
|
77
82
|
const jsonSchemaRecord = jsonSchema as Record<string, unknown>;
|
|
78
|
-
|
|
83
|
+
// Resolve a root `$ref` (e.g. caller schemas exported as `{ $ref: "#/$defs/Closed", $defs: ... }`)
|
|
84
|
+
// before deriving incremental-label metadata. AJV-style validation chases the ref at runtime, so
|
|
85
|
+
// `validate()` accepts the resolved object — but `properties` and `additionalProperties` live on
|
|
86
|
+
// the inlined node, not the wrapper. Without this, unknown labels slipped past the yield gate and
|
|
87
|
+
// only fired as parent-side schema_violations.
|
|
88
|
+
const dereferenced = dereferenceJsonSchema(jsonSchemaRecord);
|
|
89
|
+
const labelSchema =
|
|
90
|
+
dereferenced && typeof dereferenced === "object" && !Array.isArray(dereferenced)
|
|
91
|
+
? (dereferenced as Record<string, unknown>)
|
|
92
|
+
: jsonSchemaRecord;
|
|
93
|
+
const required = extractRequiredFields(labelSchema);
|
|
94
|
+
const sectionLabels = buildSectionLabelMetadata(labelSchema);
|
|
79
95
|
return {
|
|
80
96
|
normalized,
|
|
81
97
|
jsonSchema: jsonSchemaRecord,
|
|
82
98
|
validator: {
|
|
83
99
|
requiredFields: required,
|
|
84
100
|
validate: value => validateJsonSchemaValue(jsonSchemaRecord, value),
|
|
85
|
-
validateSection: buildSectionValidators(
|
|
101
|
+
validateSection: buildSectionValidators(labelSchema),
|
|
102
|
+
rejectUnknownSections: sectionLabels.rejectUnknownSections,
|
|
103
|
+
knownSectionLabels: sectionLabels.labels,
|
|
104
|
+
isKnownSection: sectionLabels.isKnown,
|
|
86
105
|
},
|
|
87
106
|
};
|
|
88
107
|
}
|
|
@@ -93,27 +112,145 @@ export function buildOutputValidator(schema: unknown): BuildOutputValidatorResul
|
|
|
93
112
|
* Each entry validates the `data` payload of one `type: ["<label>"]` section against the
|
|
94
113
|
* matching property's sub-schema — array-typed properties (e.g. `findings`, derived from JTD
|
|
95
114
|
* `elements`) use the items schema since each yield contributes one element, while scalar
|
|
96
|
-
* properties use the property schema directly.
|
|
97
|
-
*
|
|
115
|
+
* properties use the property schema directly. Closed top-level schemas reject labels that are
|
|
116
|
+
* not declared as properties.
|
|
98
117
|
*/
|
|
99
118
|
function buildSectionValidators(
|
|
100
119
|
jsonSchema: Record<string, unknown>,
|
|
101
120
|
): ReadonlyMap<string, (value: unknown) => JsonSchemaValidationResult> {
|
|
102
121
|
const validators = new Map<string, (value: unknown) => JsonSchemaValidationResult>();
|
|
103
122
|
const properties = jsonSchema.properties;
|
|
104
|
-
if (properties
|
|
105
|
-
for (const
|
|
106
|
-
|
|
107
|
-
const propRecord = raw
|
|
123
|
+
if (!isRecord(properties)) return validators;
|
|
124
|
+
for (const label in properties) {
|
|
125
|
+
const raw = properties[label];
|
|
126
|
+
const propRecord = isRecord(raw) ? raw : undefined;
|
|
108
127
|
const sectionSchema =
|
|
109
|
-
propRecord
|
|
110
|
-
?
|
|
111
|
-
:
|
|
128
|
+
propRecord?.type === "array" && propRecord.items !== undefined && propRecord.items !== null
|
|
129
|
+
? propRecord.items
|
|
130
|
+
: raw;
|
|
112
131
|
validators.set(label, value => validateJsonSchemaValue(sectionSchema, value));
|
|
113
132
|
}
|
|
114
133
|
return validators;
|
|
115
134
|
}
|
|
116
135
|
|
|
136
|
+
interface SectionLabelMetadata {
|
|
137
|
+
readonly labels: readonly string[];
|
|
138
|
+
readonly rejectUnknownSections: boolean;
|
|
139
|
+
isKnown(label: string): boolean;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Derive incremental-label metadata from top-level schema closure.
|
|
144
|
+
*
|
|
145
|
+
* The unknown-label gate (`rejectUnknownSections`) engages when the schema constrains top-level
|
|
146
|
+
* property names anywhere: a closed conjunct (root or recursive `allOf` child with
|
|
147
|
+
* `additionalProperties: false`) or a `oneOf`/`anyOf` union whose EVERY variant is closed. A label
|
|
148
|
+
* is known iff every closed conjunct accepts it AND, per closed union, at least one variant
|
|
149
|
+
* accepts it (union semantics are disjunctive — the assembled output only has to match one
|
|
150
|
+
* variant). Unions containing any open variant never gate: the open variant accepts arbitrary
|
|
151
|
+
* labels, so rejection would be a false positive.
|
|
152
|
+
*/
|
|
153
|
+
function buildSectionLabelMetadata(jsonSchema: Record<string, unknown>): SectionLabelMetadata {
|
|
154
|
+
const closedConjuncts = collectClosedTopLevelSchemas(jsonSchema);
|
|
155
|
+
const closedUnions = collectClosedTopLevelUnions(jsonSchema);
|
|
156
|
+
const closed = closedConjuncts.length > 0 || closedUnions.length > 0;
|
|
157
|
+
const acceptedByAll = (conjuncts: readonly Record<string, unknown>[], label: string): boolean =>
|
|
158
|
+
conjuncts.every(schema => schemaAcceptsSectionLabel(schema, label));
|
|
159
|
+
const labels = [
|
|
160
|
+
...new Set([
|
|
161
|
+
...closedConjuncts.flatMap(schema => declaredPropertyLabels(schema)),
|
|
162
|
+
...closedUnions.flatMap(variants =>
|
|
163
|
+
variants.flatMap(conjuncts => conjuncts.flatMap(schema => declaredPropertyLabels(schema))),
|
|
164
|
+
),
|
|
165
|
+
]),
|
|
166
|
+
];
|
|
167
|
+
return {
|
|
168
|
+
labels,
|
|
169
|
+
rejectUnknownSections: closed,
|
|
170
|
+
isKnown: label =>
|
|
171
|
+
!closed ||
|
|
172
|
+
(acceptedByAll(closedConjuncts, label) &&
|
|
173
|
+
closedUnions.every(variants => variants.some(conjuncts => acceptedByAll(conjuncts, label)))),
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function collectClosedTopLevelSchemas(jsonSchema: Record<string, unknown>): Record<string, unknown>[] {
|
|
178
|
+
const schemas: Record<string, unknown>[] = [];
|
|
179
|
+
if (jsonSchema.additionalProperties === false) schemas.push(jsonSchema);
|
|
180
|
+
const allOf = jsonSchema.allOf;
|
|
181
|
+
if (Array.isArray(allOf)) {
|
|
182
|
+
for (const raw of allOf) {
|
|
183
|
+
if (raw !== null && typeof raw === "object" && !Array.isArray(raw)) {
|
|
184
|
+
schemas.push(...collectClosedTopLevelSchemas(raw as Record<string, unknown>));
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
return schemas;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/** One fully-closed `oneOf`/`anyOf` union: per variant, that variant's closed conjunct schemas. */
|
|
192
|
+
type ClosedUnionVariants = Record<string, unknown>[][];
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Collect top-level `oneOf`/`anyOf` unions in which EVERY variant is closed — i.e. each variant
|
|
196
|
+
* (or one of its `allOf` conjuncts, resolved via `collectClosedTopLevelSchemas`) carries
|
|
197
|
+
* `additionalProperties: false`. JTD discriminator output schemas compile to exactly this shape:
|
|
198
|
+
* a root `oneOf` of closed object variants. Unions with any open (or non-object) variant are
|
|
199
|
+
* skipped entirely so the unknown-label gate cannot fire false rejections. Unions nested under
|
|
200
|
+
* `allOf` conjuncts gate identically (intersection semantics).
|
|
201
|
+
*/
|
|
202
|
+
function collectClosedTopLevelUnions(jsonSchema: Record<string, unknown>): ClosedUnionVariants[] {
|
|
203
|
+
const unions: ClosedUnionVariants[] = [];
|
|
204
|
+
for (const key of ["oneOf", "anyOf"] as const) {
|
|
205
|
+
const rawVariants = jsonSchema[key];
|
|
206
|
+
if (!Array.isArray(rawVariants) || rawVariants.length === 0) continue;
|
|
207
|
+
const variants: ClosedUnionVariants = [];
|
|
208
|
+
let allClosed = true;
|
|
209
|
+
for (const raw of rawVariants) {
|
|
210
|
+
const conjuncts = isRecord(raw) ? collectClosedTopLevelSchemas(raw) : [];
|
|
211
|
+
if (conjuncts.length === 0) {
|
|
212
|
+
allClosed = false;
|
|
213
|
+
break;
|
|
214
|
+
}
|
|
215
|
+
variants.push(conjuncts);
|
|
216
|
+
}
|
|
217
|
+
if (allClosed) unions.push(variants);
|
|
218
|
+
}
|
|
219
|
+
const allOf = jsonSchema.allOf;
|
|
220
|
+
if (Array.isArray(allOf)) {
|
|
221
|
+
for (const raw of allOf) {
|
|
222
|
+
if (isRecord(raw)) unions.push(...collectClosedTopLevelUnions(raw));
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
return unions;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
function declaredPropertyLabels(jsonSchema: Record<string, unknown>): string[] {
|
|
229
|
+
const properties = jsonSchema.properties;
|
|
230
|
+
if (properties === null || typeof properties !== "object" || Array.isArray(properties)) return [];
|
|
231
|
+
const labels: string[] = [];
|
|
232
|
+
for (const label in properties) labels.push(label);
|
|
233
|
+
return labels;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
function schemaAcceptsSectionLabel(jsonSchema: Record<string, unknown>, label: string): boolean {
|
|
237
|
+
const properties = jsonSchema.properties;
|
|
238
|
+
if (properties !== null && typeof properties === "object" && !Array.isArray(properties) && label in properties) {
|
|
239
|
+
return true;
|
|
240
|
+
}
|
|
241
|
+
const patternProperties = jsonSchema.patternProperties;
|
|
242
|
+
if (patternProperties !== null && typeof patternProperties === "object" && !Array.isArray(patternProperties)) {
|
|
243
|
+
for (const pattern in patternProperties) {
|
|
244
|
+
try {
|
|
245
|
+
if (new RegExp(pattern).test(label)) return true;
|
|
246
|
+
} catch {
|
|
247
|
+
// `isValidJsonSchema` already rejected malformed regexes; ignore any unexpected runtime mismatch.
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
return jsonSchema.additionalProperties !== false;
|
|
252
|
+
}
|
|
253
|
+
|
|
117
254
|
/** Produce the executor's headline+missing-required summary from a failed validation. */
|
|
118
255
|
export function summarizeValidationFailure(
|
|
119
256
|
result: JsonSchemaValidationResult,
|
package/src/tools/path-utils.ts
CHANGED
|
@@ -2,7 +2,7 @@ import * as fs from "node:fs";
|
|
|
2
2
|
import * as os from "node:os";
|
|
3
3
|
import * as path from "node:path";
|
|
4
4
|
import * as url from "node:url";
|
|
5
|
-
import { isEnoent, stripWindowsExtendedLengthPathPrefix } from "@oh-my-pi/pi-utils";
|
|
5
|
+
import { isEnoent, isEnotdir, stripWindowsExtendedLengthPathPrefix } from "@oh-my-pi/pi-utils";
|
|
6
6
|
import type { Skill } from "../extensibility/skills";
|
|
7
7
|
import { InternalUrlRouter, type LocalProtocolOptions } from "../internal-urls";
|
|
8
8
|
import { ToolError } from "./tool-errors";
|
|
@@ -435,6 +435,16 @@ export function isSshUrl(path: string): boolean {
|
|
|
435
435
|
return /^ssh:\/\//i.test(path.trim());
|
|
436
436
|
}
|
|
437
437
|
|
|
438
|
+
/**
|
|
439
|
+
* True when the read tool's URL parser (`parseReadUrlTarget` in fetch.ts) would
|
|
440
|
+
* recognize this path as a readable external URL: a strict `http(s)://`, a
|
|
441
|
+
* collapsed `http(s):/host` (Node path normalization folds `//` → `/`), or a
|
|
442
|
+
* scheme-less `www.` spelling. Keep in sync with `parseReadUrlTarget`.
|
|
443
|
+
*/
|
|
444
|
+
export function isReadableUrlPath(value: string): boolean {
|
|
445
|
+
return /^https?:\/\/?/i.test(value) || /^www\./i.test(value);
|
|
446
|
+
}
|
|
447
|
+
|
|
438
448
|
/**
|
|
439
449
|
* Resolve a path relative to the given cwd.
|
|
440
450
|
* Handles ~ expansion and absolute paths.
|
|
@@ -1026,6 +1036,14 @@ export function resolveReadPath(filePath: string, cwd: string): string {
|
|
|
1026
1036
|
// Tool-scope resolution (search/ast tools)
|
|
1027
1037
|
// =============================================================================
|
|
1028
1038
|
|
|
1039
|
+
/** Local file materialized from a readable external URL for shared tool-scope resolution. */
|
|
1040
|
+
export interface ResolvedExternalSearchUrl {
|
|
1041
|
+
/** Absolute or cwd-relative file path to search. */
|
|
1042
|
+
sourcePath: string;
|
|
1043
|
+
/** True when the materialized file must not mint editable anchors. */
|
|
1044
|
+
immutable?: boolean;
|
|
1045
|
+
}
|
|
1046
|
+
|
|
1029
1047
|
export interface ToolScopeOptions {
|
|
1030
1048
|
rawPaths: string[];
|
|
1031
1049
|
cwd: string;
|
|
@@ -1049,6 +1067,8 @@ export interface ToolScopeOptions {
|
|
|
1049
1067
|
localProtocolOptions?: LocalProtocolOptions;
|
|
1050
1068
|
/** Calling session's loaded skills — lets skill:// resolve without process-global state. */
|
|
1051
1069
|
skills?: readonly Skill[];
|
|
1070
|
+
/** Materialize readable external URLs to local text files before scope derivation. */
|
|
1071
|
+
resolveExternalUrl?: (rawPath: string) => Promise<ResolvedExternalSearchUrl | undefined>;
|
|
1052
1072
|
}
|
|
1053
1073
|
|
|
1054
1074
|
export interface ToolScopeResolution {
|
|
@@ -1080,19 +1100,44 @@ export async function resolveToolSearchScope(opts: ToolScopeOptions): Promise<To
|
|
|
1080
1100
|
if (rawPaths.some(rawPath => rawPath.length === 0)) {
|
|
1081
1101
|
throw new ToolError("`paths` must contain non-empty paths or globs");
|
|
1082
1102
|
}
|
|
1083
|
-
//
|
|
1084
|
-
//
|
|
1085
|
-
//
|
|
1086
|
-
const
|
|
1087
|
-
if (externalUrl) {
|
|
1088
|
-
throw new ToolError(
|
|
1089
|
-
`Cannot ${internalUrlAction} external URL: ${externalUrl}. Use \`read\` to fetch web content, then search the returned text.`,
|
|
1090
|
-
);
|
|
1091
|
-
}
|
|
1103
|
+
// Strict external-URL schemes. `file://` is intentionally absent: it has
|
|
1104
|
+
// local-path semantics (expandPath strips it downstream), so it flows through
|
|
1105
|
+
// the ordinary filesystem pipeline instead of the external-URL resolver.
|
|
1106
|
+
const strictExternalUrlRe = /^(?:https?|ftp|ws|wss):\/\//i;
|
|
1092
1107
|
const internalRouter = InternalUrlRouter.instance();
|
|
1093
1108
|
const resolvedPathInputs: string[] = [];
|
|
1094
1109
|
const immutableSourcePaths = new Set<string>();
|
|
1095
1110
|
for (const rawPath of rawPaths) {
|
|
1111
|
+
let externalUrl = strictExternalUrlRe.test(rawPath);
|
|
1112
|
+
if (!externalUrl && isReadableUrlPath(rawPath) && !hasGlobPathChars(rawPath)) {
|
|
1113
|
+
// Fuzzy spelling the read parser accepts (`www.host/…`, collapsed
|
|
1114
|
+
// `https:/host/…`). An existing local path wins over URL
|
|
1115
|
+
// interpretation so a directory literally named `www.foo` stays
|
|
1116
|
+
// searchable; only a definitive ENOENT/ENOTDIR flips to URL handling
|
|
1117
|
+
// (any other stat error means the path exists — let the local
|
|
1118
|
+
// pipeline surface it).
|
|
1119
|
+
try {
|
|
1120
|
+
await fs.promises.stat(resolveToCwd(rawPath, cwd));
|
|
1121
|
+
} catch (err) {
|
|
1122
|
+
externalUrl = isEnoent(err) || isEnotdir(err);
|
|
1123
|
+
}
|
|
1124
|
+
}
|
|
1125
|
+
if (externalUrl) {
|
|
1126
|
+
const resolved = opts.resolveExternalUrl ? await opts.resolveExternalUrl(rawPath) : undefined;
|
|
1127
|
+
if (resolved) {
|
|
1128
|
+
resolvedPathInputs.push(resolved.sourcePath);
|
|
1129
|
+
if (opts.trackImmutableSources && resolved.immutable) {
|
|
1130
|
+
immutableSourcePaths.add(path.resolve(resolved.sourcePath));
|
|
1131
|
+
}
|
|
1132
|
+
continue;
|
|
1133
|
+
}
|
|
1134
|
+
// Resolver missing or declined (e.g. ftp/ws/wss): fail explicitly
|
|
1135
|
+
// instead of letting the local-path fallthrough surface a confusing
|
|
1136
|
+
// "Path not found" for a URL-shaped input.
|
|
1137
|
+
throw new ToolError(
|
|
1138
|
+
`Cannot ${internalUrlAction} external URL: ${rawPath}. Use \`read\` to fetch web content, then search the returned text.`,
|
|
1139
|
+
);
|
|
1140
|
+
}
|
|
1096
1141
|
if (!internalRouter.canHandle(rawPath)) {
|
|
1097
1142
|
resolvedPathInputs.push(rawPath);
|
|
1098
1143
|
continue;
|
package/src/tools/read.ts
CHANGED
|
@@ -77,7 +77,6 @@ import {
|
|
|
77
77
|
} from "./conflict-detect";
|
|
78
78
|
import {
|
|
79
79
|
executeReadUrl,
|
|
80
|
-
isReadableUrlPath,
|
|
81
80
|
loadReadUrlCacheEntry,
|
|
82
81
|
parseReadUrlTarget,
|
|
83
82
|
type ReadUrlToolDetails,
|
|
@@ -95,6 +94,7 @@ import {
|
|
|
95
94
|
import {
|
|
96
95
|
expandPath,
|
|
97
96
|
formatPathRelativeToCwd,
|
|
97
|
+
isReadableUrlPath,
|
|
98
98
|
type LineRange,
|
|
99
99
|
parseLineRanges,
|
|
100
100
|
pathTargetsSsh,
|
|
@@ -222,19 +222,21 @@ export function previewWindowRows(): number {
|
|
|
222
222
|
* (ctrl+o) uncaps it.
|
|
223
223
|
*
|
|
224
224
|
* `prefix` (raw, e.g. a dim tree gutter) is prepended to the marker line so
|
|
225
|
-
* nested previews stay aligned.
|
|
225
|
+
* nested previews stay aligned. `expandHint: false` drops the "ctrl+o: Expand"
|
|
226
|
+
* suffix for callers that cap even inside the expanded view (task recent
|
|
227
|
+
* output), where the hint would point the wrong way.
|
|
226
228
|
*/
|
|
227
229
|
export function capPreviewLines(
|
|
228
230
|
lines: string[],
|
|
229
231
|
theme: Theme,
|
|
230
|
-
options: { max?: number; expanded?: boolean; prefix?: string } = {},
|
|
232
|
+
options: { max?: number; expanded?: boolean; prefix?: string; expandHint?: boolean } = {},
|
|
231
233
|
): string[] {
|
|
232
234
|
if (options.expanded) return lines;
|
|
233
235
|
const max = options.max ?? previewWindowRows();
|
|
234
236
|
if (lines.length <= max) return lines;
|
|
235
237
|
const visible = max <= 1 ? [] : lines.slice(lines.length - (max - 1));
|
|
236
238
|
const hidden = lines.length - visible.length;
|
|
237
|
-
const hint = formatExpandHint(theme, false, true);
|
|
239
|
+
const hint = options.expandHint === false ? "" : formatExpandHint(theme, false, true);
|
|
238
240
|
const marker = `… ${hidden} earlier ${pluralize("line", hidden)}${hint ? ` ${hint}` : ""}`;
|
|
239
241
|
return [`${options.prefix ?? ""}${theme.fg("dim", marker)}`, ...visible];
|
|
240
242
|
}
|
package/src/tools/yield.ts
CHANGED
|
@@ -213,11 +213,17 @@ export class YieldTool implements AgentTool<TSchema, YieldDetails> {
|
|
|
213
213
|
|
|
214
214
|
readonly #validate?: (value: unknown) => JsonSchemaValidationResult;
|
|
215
215
|
readonly #validateSection?: ReadonlyMap<string, (value: unknown) => JsonSchemaValidationResult>;
|
|
216
|
+
#rejectUnknownSections = false;
|
|
217
|
+
#knownSectionLabels: readonly string[] = [];
|
|
218
|
+
#isKnownSection?: (label: string) => boolean;
|
|
216
219
|
#schemaValidationFailures = 0;
|
|
217
220
|
|
|
218
221
|
constructor(session: ToolSession) {
|
|
219
222
|
let validate: ((value: unknown) => JsonSchemaValidationResult) | undefined;
|
|
220
223
|
let validateSection: ReadonlyMap<string, (value: unknown) => JsonSchemaValidationResult> | undefined;
|
|
224
|
+
let rejectUnknownSections = false;
|
|
225
|
+
let knownSectionLabels: readonly string[] = [];
|
|
226
|
+
let isKnownSection: ((label: string) => boolean) | undefined;
|
|
221
227
|
let parameters: TSchema;
|
|
222
228
|
|
|
223
229
|
try {
|
|
@@ -230,6 +236,9 @@ export class YieldTool implements AgentTool<TSchema, YieldDetails> {
|
|
|
230
236
|
if (validator) {
|
|
231
237
|
validate = value => validator.validate(value);
|
|
232
238
|
validateSection = validator.validateSection;
|
|
239
|
+
rejectUnknownSections = validator.rejectUnknownSections;
|
|
240
|
+
knownSectionLabels = validator.knownSectionLabels;
|
|
241
|
+
isKnownSection = label => validator.isKnownSection(label);
|
|
233
242
|
}
|
|
234
243
|
|
|
235
244
|
const schemaHint = formatSchema(normalizedSchema ?? session.outputSchema);
|
|
@@ -280,6 +289,9 @@ export class YieldTool implements AgentTool<TSchema, YieldDetails> {
|
|
|
280
289
|
|
|
281
290
|
this.#validate = validate;
|
|
282
291
|
this.#validateSection = validateSection;
|
|
292
|
+
this.#rejectUnknownSections = rejectUnknownSections;
|
|
293
|
+
this.#knownSectionLabels = knownSectionLabels;
|
|
294
|
+
this.#isKnownSection = isKnownSection;
|
|
283
295
|
this.parameters = parameters;
|
|
284
296
|
}
|
|
285
297
|
|
|
@@ -317,6 +329,21 @@ export class YieldTool implements AgentTool<TSchema, YieldDetails> {
|
|
|
317
329
|
|
|
318
330
|
const status = errorMessage !== undefined ? "aborted" : "success";
|
|
319
331
|
let schemaValidationOverridden = false;
|
|
332
|
+
// Unknown incremental labels are a hard contract mismatch with the closed caller
|
|
333
|
+
// schema. Reject before the last-turn short-circuit too: `type: ["findings"], result: {}`
|
|
334
|
+
// would otherwise be accepted as a typed last-turn incremental yield, then a sibling
|
|
335
|
+
// section's MAX_SCHEMA_RETRIES override flips schemaOverridden in finalization and the
|
|
336
|
+
// stale section rides along untouched.
|
|
337
|
+
if (status === "success" && isIncremental) {
|
|
338
|
+
const unknownLabels = this.#unknownIncrementalLabels(yieldType as string[]);
|
|
339
|
+
if (unknownLabels.length > 0) {
|
|
340
|
+
const validLabels =
|
|
341
|
+
this.#knownSectionLabels.length > 0 ? formatYieldLabels(this.#knownSectionLabels) : "none";
|
|
342
|
+
throw new Error(
|
|
343
|
+
`Section ${formatYieldLabels(yieldType as string[])} uses unknown incremental yield label(s): ${formatYieldLabels(unknownLabels)}. Resubmit with one of the schema's labels: ${validLabels}.`,
|
|
344
|
+
);
|
|
345
|
+
}
|
|
346
|
+
}
|
|
320
347
|
if (status === "success" && !useLastTurn) {
|
|
321
348
|
if (data === null) {
|
|
322
349
|
throw new Error("data is required when yield indicates success");
|
|
@@ -362,13 +389,26 @@ export class YieldTool implements AgentTool<TSchema, YieldDetails> {
|
|
|
362
389
|
};
|
|
363
390
|
}
|
|
364
391
|
|
|
392
|
+
/**
|
|
393
|
+
* Return incremental yield labels the closed caller schema does not accept. Closure covers the
|
|
394
|
+
* root, `allOf` conjuncts, and `oneOf`/`anyOf` unions whose every variant is closed (e.g. JTD
|
|
395
|
+
* discriminators). Open schemas accept any label.
|
|
396
|
+
*/
|
|
397
|
+
#unknownIncrementalLabels(labels: string[]): string[] {
|
|
398
|
+
if (!this.#rejectUnknownSections) return [];
|
|
399
|
+
const isKnown = this.#isKnownSection;
|
|
400
|
+
if (!isKnown) return [];
|
|
401
|
+
return labels.filter(label => !isKnown(label));
|
|
402
|
+
}
|
|
403
|
+
|
|
365
404
|
/**
|
|
366
405
|
* Validate the `data` payload of an incremental yield (`type: ["<label>", …]`) against
|
|
367
406
|
* the matching property's sub-validator. Returns the first failure across all known labels,
|
|
368
407
|
* or `undefined` when no label is recognised (user-defined section labels stay loose) or
|
|
369
408
|
* when all known labels accept the value. Lets the model see the same retry feedback that
|
|
370
409
|
* the terminal-yield path already produces, instead of leaking the mismatch through to
|
|
371
|
-
* the parent's post-mortem `schema_violation`.
|
|
410
|
+
* the parent's post-mortem `schema_violation`. Unknown labels under a closed schema are
|
|
411
|
+
* handled separately by `#unknownIncrementalLabels` and never reach this validator.
|
|
372
412
|
*/
|
|
373
413
|
#validateIncrementalSection(labels: string[], data: unknown): JsonSchemaValidationResult | undefined {
|
|
374
414
|
const subValidators = this.#validateSection;
|
|
@@ -12,7 +12,7 @@ import { getModelMatchPreferences, resolveModelRoleValue } from "../config/model
|
|
|
12
12
|
import type { Settings } from "../config/settings";
|
|
13
13
|
import MODEL_PRIO from "../priority.json" with { type: "json" };
|
|
14
14
|
import commitSystemPrompt from "../prompts/system/commit-message-system.md" with { type: "text" };
|
|
15
|
-
import { toReasoningEffort } from "../thinking";
|
|
15
|
+
import { concreteThinkingLevel, toReasoningEffort } from "../thinking";
|
|
16
16
|
|
|
17
17
|
const COMMIT_SYSTEM_PROMPT = prompt.render(commitSystemPrompt);
|
|
18
18
|
const MAX_DIFF_CHARS = 4000;
|
|
@@ -56,7 +56,7 @@ function getSmolModelCandidates(
|
|
|
56
56
|
settings,
|
|
57
57
|
matchPreferences,
|
|
58
58
|
});
|
|
59
|
-
addCandidate(configuredSmol.model, configuredSmol.thinkingLevel);
|
|
59
|
+
addCandidate(configuredSmol.model, concreteThinkingLevel(configuredSmol.thinkingLevel));
|
|
60
60
|
|
|
61
61
|
for (const pattern of MODEL_PRIO.smol) {
|
|
62
62
|
const needle = pattern.toLowerCase();
|