jeopi-cli 16.4.3 → 16.4.4
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/dist/cli.js +3094 -3091
- package/dist/types/advisor/advise-tool.d.ts +5 -1
- package/dist/types/advisor/runtime.d.ts +15 -0
- package/dist/types/config/model-registry.d.ts +3 -3
- package/dist/types/config/models-config.d.ts +1 -1
- package/dist/types/config/settings-schema.d.ts +0 -10
- package/dist/types/modes/rpc/host-tools.d.ts +2 -0
- package/dist/types/modes/rpc/rpc-mode.d.ts +27 -7
- package/dist/types/modes/utils/transcript-render-helpers.d.ts +9 -0
- package/dist/types/session/agent-session.d.ts +1 -0
- package/package.json +12 -12
- package/src/advisor/advise-tool.ts +6 -1
- package/src/advisor/runtime.ts +127 -0
- package/src/autolearn/controller.ts +1 -1
- package/src/config/config-file.ts +30 -12
- package/src/config/model-registry.ts +7 -7
- package/src/config/models-config.ts +1 -1
- package/src/config/settings-schema.ts +0 -12
- package/src/extensibility/plugins/legacy-pi-bundled-keys.ts +9 -1
- package/src/extensibility/plugins/legacy-pi-bundled-registry.ts +30 -3
- package/src/internal-urls/docs-index.generated.txt +1 -1
- package/src/modes/components/chat-transcript-builder.ts +31 -11
- package/src/modes/controllers/event-controller.ts +41 -12
- package/src/modes/rpc/host-tools.ts +13 -0
- package/src/modes/rpc/rpc-mode.ts +130 -56
- package/src/modes/utils/transcript-render-helpers.ts +44 -0
- package/src/modes/utils/ui-helpers.ts +16 -1
- package/src/session/agent-session.ts +95 -11
- package/src/task/render.ts +51 -44
- package/src/tools/bash.ts +0 -11
- package/src/tools/path-utils.ts +6 -1
- package/src/utils/title-generator.ts +5 -1
- package/dist/types/tools/bash-command-fixup.d.ts +0 -11
- package/src/tools/bash-command-fixup.ts +0 -37
|
@@ -51,6 +51,9 @@ export declare function isAdvisorInterruptImmuneTurnActive(opts: {
|
|
|
51
51
|
* - An interrupting `concern`/`blocker` is normally steered into the agent: into
|
|
52
52
|
* the live turn while one is streaming, or (when idle) a triggered turn so the
|
|
53
53
|
* advice is acted on immediately.
|
|
54
|
+
* - If the primary tail is already a terminal text answer and there is no queued
|
|
55
|
+
* work, late interrupting advice is preserved as a visible card instead of
|
|
56
|
+
* waking the primary to restate completion.
|
|
54
57
|
* - After a deliberate user interrupt (`autoResumeSuppressed`) the advisor must
|
|
55
58
|
* not auto-resume the stopped run. While the agent is idle — or still tearing
|
|
56
59
|
* the interrupted turn down (`aborting`) — the note is preserved as a visible
|
|
@@ -60,13 +63,14 @@ export declare function isAdvisorInterruptImmuneTurnActive(opts: {
|
|
|
60
63
|
* run instead strands it (it never reaches the running agent) and the withheld
|
|
61
64
|
* notes dump as one burst at the next user prompt — the bug this guards.
|
|
62
65
|
* - During the post-interrupt immune-turn window, further `concern`/`blocker`
|
|
63
|
-
* notes are downgraded to asides;
|
|
66
|
+
* notes are downgraded to asides; preservation still wins.
|
|
64
67
|
*/
|
|
65
68
|
export declare function resolveAdvisorDeliveryChannel(opts: {
|
|
66
69
|
severity: AdvisorSeverity | undefined;
|
|
67
70
|
autoResumeSuppressed: boolean;
|
|
68
71
|
streaming: boolean;
|
|
69
72
|
aborting: boolean;
|
|
73
|
+
terminalAnswerNoQueuedWork?: boolean;
|
|
70
74
|
interruptImmuneTurnActive?: boolean;
|
|
71
75
|
}): AdvisorDeliveryChannel;
|
|
72
76
|
/**
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { AgentMessage } from "jeopi-agent-core";
|
|
2
|
+
import type { AssistantMessage } from "jeopi-ai";
|
|
2
3
|
import { type SecretObfuscator } from "../secrets/obfuscator";
|
|
3
4
|
/**
|
|
4
5
|
* Minimal slice of `Agent` the runtime drives — satisfied by pi-agent-core
|
|
@@ -58,6 +59,20 @@ export interface AdvisorRuntimeOptions {
|
|
|
58
59
|
/** Render the primary's thinking blocks into advisor deltas (default true). */
|
|
59
60
|
includeThinking?: boolean;
|
|
60
61
|
}
|
|
62
|
+
/** Signals that an advisor response was discarded before it could become model-visible context. */
|
|
63
|
+
export declare class AdvisorOutputQuarantinedError extends Error {
|
|
64
|
+
constructor(message: string);
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Replaces an advisor assistant turn that requested unavailable tools or generated
|
|
68
|
+
* output-only destructive directives with a sanitized error before dispatch.
|
|
69
|
+
*/
|
|
70
|
+
export declare function quarantineAdvisorUnsafeOutput(message: AssistantMessage, availableToolNames: ReadonlySet<string>, sourceText?: string): string | undefined;
|
|
71
|
+
/**
|
|
72
|
+
* Builds the provenance text used to decide whether hazardous advisor output was
|
|
73
|
+
* generated by the advisor or came from model-visible primary/tool context.
|
|
74
|
+
*/
|
|
75
|
+
export declare function buildAdvisorQuarantineSourceText(currentInput: string, messages: readonly AgentMessage[]): string;
|
|
61
76
|
export declare function isRefusalError(error: unknown): boolean;
|
|
62
77
|
export declare class AdvisorRuntime {
|
|
63
78
|
#private;
|
|
@@ -74,7 +74,7 @@ export declare class ModelRegistry {
|
|
|
74
74
|
fetch?: FetchImpl;
|
|
75
75
|
});
|
|
76
76
|
/**
|
|
77
|
-
* Reload models from disk (built-in + custom
|
|
77
|
+
* Reload models from disk (built-in + custom config).
|
|
78
78
|
*/
|
|
79
79
|
refresh(strategy?: ModelRefreshStrategy): Promise<void>;
|
|
80
80
|
refreshInBackground(strategy?: ModelRefreshStrategy): void;
|
|
@@ -94,12 +94,12 @@ export declare class ModelRegistry {
|
|
|
94
94
|
*/
|
|
95
95
|
refreshRuntimeProviders(strategy?: ModelRefreshStrategy): Promise<void>;
|
|
96
96
|
/**
|
|
97
|
-
* Get any error from loading models
|
|
97
|
+
* Get any error from loading custom models config (undefined if no error).
|
|
98
98
|
*/
|
|
99
99
|
getError(): ConfigError | undefined;
|
|
100
100
|
/**
|
|
101
101
|
* Get all models (built-in + custom).
|
|
102
|
-
* If
|
|
102
|
+
* If custom config had errors, returns only built-in models.
|
|
103
103
|
*/
|
|
104
104
|
getAll(): Model<Api>[];
|
|
105
105
|
/**
|
|
@@ -3332,16 +3332,6 @@ export declare const SETTINGS_SCHEMA: {
|
|
|
3332
3332
|
readonly type: "array";
|
|
3333
3333
|
readonly default: BashInterceptorRule[];
|
|
3334
3334
|
};
|
|
3335
|
-
readonly "bash.stripTrailingHeadTail": {
|
|
3336
|
-
readonly type: "boolean";
|
|
3337
|
-
readonly default: true;
|
|
3338
|
-
readonly ui: {
|
|
3339
|
-
readonly tab: "shell";
|
|
3340
|
-
readonly group: "Bash";
|
|
3341
|
-
readonly label: "Strip head/tail Pipes";
|
|
3342
|
-
readonly description: "Silently drop trailing `| head`/`| tail` pipes from single-line bash commands. Output is already truncated automatically.";
|
|
3343
|
-
};
|
|
3344
|
-
};
|
|
3345
3335
|
readonly "shellMinimizer.enabled": {
|
|
3346
3336
|
readonly type: "boolean";
|
|
3347
3337
|
readonly default: true;
|
|
@@ -12,5 +12,7 @@ export declare class RpcHostToolBridge {
|
|
|
12
12
|
handleUpdate(frame: RpcHostToolUpdate): boolean;
|
|
13
13
|
requestExecution(definition: RpcHostToolDefinition, toolCallId: string, args: Record<string, unknown>, signal?: AbortSignal, onUpdate?: AgentToolUpdateCallback<unknown>): Promise<AgentToolResult<unknown>>;
|
|
14
14
|
rejectAllPending(message: string): void;
|
|
15
|
+
/** Reject active and future host tool requests after the RPC client disconnects. */
|
|
16
|
+
close(message: string): void;
|
|
15
17
|
}
|
|
16
18
|
export {};
|
|
@@ -8,6 +8,13 @@ export type PendingExtensionRequest = {
|
|
|
8
8
|
resolve: (response: RpcExtensionUIResponse) => void;
|
|
9
9
|
reject: (error: Error) => void;
|
|
10
10
|
};
|
|
11
|
+
/** Pending extension UI request map that can fail closed when the RPC client disconnects. */
|
|
12
|
+
export declare class RpcPendingExtensionRequests extends Map<string, PendingExtensionRequest> {
|
|
13
|
+
#private;
|
|
14
|
+
set(id: string, request: PendingExtensionRequest): this;
|
|
15
|
+
/** Reject every active and future extension UI request. */
|
|
16
|
+
rejectAll(message: string): void;
|
|
17
|
+
}
|
|
11
18
|
type RpcOutput = (obj: RpcResponse | RpcExtensionUIRequest | RpcHostToolCallRequest | RpcHostToolCancelRequest | RpcHostUriRequest | RpcHostUriCancelRequest | object) => void;
|
|
12
19
|
export type RpcSessionChangeCommand = Extract<RpcCommand, {
|
|
13
20
|
type: "new_session";
|
|
@@ -38,7 +45,7 @@ export type RpcSkillCommandSession = Pick<AgentSession, "promptCustomMessage" |
|
|
|
38
45
|
export type RpcSkillCommandResult = {
|
|
39
46
|
agentInvoked: true;
|
|
40
47
|
};
|
|
41
|
-
export declare function tryRunRpcSkillCommand(session: RpcSkillCommandSession, text: string): Promise<RpcSkillCommandResult | false>;
|
|
48
|
+
export declare function tryRunRpcSkillCommand(session: RpcSkillCommandSession, text: string, streamingBehavior?: "steer" | "followUp"): Promise<RpcSkillCommandResult | false>;
|
|
42
49
|
export declare function reportLocalOnlyPromptResult(input: {
|
|
43
50
|
id: string | undefined;
|
|
44
51
|
prompt: Promise<boolean>;
|
|
@@ -84,15 +91,16 @@ export interface RpcInputFrameDeps {
|
|
|
84
91
|
onHostToolUpdate: (frame: RpcHostToolUpdate) => void;
|
|
85
92
|
onHostUriResult: (frame: RpcHostUriResult) => void;
|
|
86
93
|
}
|
|
94
|
+
/** Dispatch side-channel frames that must overtake the serialized command queue. */
|
|
95
|
+
export declare function dispatchRpcControlFrame(parsed: unknown, deps: RpcInputFrameDeps): boolean;
|
|
87
96
|
/**
|
|
88
97
|
* Dispatch a single parsed frame from the RPC input stream.
|
|
89
98
|
*
|
|
90
|
-
* Bash commands are dispatched in the background so the caller
|
|
91
|
-
*
|
|
92
|
-
*
|
|
93
|
-
*
|
|
94
|
-
*
|
|
95
|
-
* not guaranteed and clients MUST match on `id`.
|
|
99
|
+
* Bash commands are dispatched in the background so the caller can keep reading
|
|
100
|
+
* subsequent frames while a shell command is still running. This lets a client
|
|
101
|
+
* send `abort_bash` while a long-running `bash` is in flight. Response
|
|
102
|
+
* correlation is preserved via each command's `id`; ordering across concurrent
|
|
103
|
+
* commands is not guaranteed and clients MUST match on `id`.
|
|
96
104
|
*
|
|
97
105
|
* @returns `undefined` when the frame was routed to a side-channel handler
|
|
98
106
|
* (extension UI response, host tool/URI frames) or dispatched in the
|
|
@@ -101,6 +109,18 @@ export interface RpcInputFrameDeps {
|
|
|
101
109
|
* on non-`bash` commands propagate; the caller is expected to wrap them.
|
|
102
110
|
*/
|
|
103
111
|
export declare function dispatchRpcInputFrame(parsed: unknown, deps: RpcInputFrameDeps): Promise<void> | undefined;
|
|
112
|
+
/** Serializes ordinary RPC commands while allowing control frames to dispatch immediately. */
|
|
113
|
+
export declare class RpcInputDispatcher {
|
|
114
|
+
#private;
|
|
115
|
+
constructor(options: {
|
|
116
|
+
deps: RpcInputFrameDeps;
|
|
117
|
+
afterSerialCommand?: () => Promise<void>;
|
|
118
|
+
});
|
|
119
|
+
/** Accept a parsed input frame without blocking the stdin reader. */
|
|
120
|
+
dispatch(parsed: unknown): void;
|
|
121
|
+
/** Await every accepted serial command, including commands queued before EOF. */
|
|
122
|
+
drain(): Promise<void>;
|
|
123
|
+
}
|
|
104
124
|
/**
|
|
105
125
|
* Coordinates deferred shutdown with in-flight background input tasks.
|
|
106
126
|
*
|
|
@@ -38,6 +38,15 @@ export declare function buildFileMentionBlock(files: FileMentionMessage["files"]
|
|
|
38
38
|
* canonicalization) — i.e. content that closes the current read-tool run.
|
|
39
39
|
*/
|
|
40
40
|
export declare function assistantHasVisibleContent(message: AssistantAgentMessage): boolean;
|
|
41
|
+
/**
|
|
42
|
+
* Split mixed assistant turns into visible text before tool execution and
|
|
43
|
+
* visible text segments that must render immediately after the preceding tool.
|
|
44
|
+
*/
|
|
45
|
+
export declare function splitAssistantMessageToolTimeline(message: AssistantAgentMessage): {
|
|
46
|
+
beforeTools: AssistantAgentMessage;
|
|
47
|
+
afterToolCalls: ReadonlyMap<string, AssistantAgentMessage>;
|
|
48
|
+
hasToolCalls: boolean;
|
|
49
|
+
};
|
|
41
50
|
/**
|
|
42
51
|
* Normalize raw tool-call arguments to a plain record, collapsing non-object or
|
|
43
52
|
* array values to an empty object.
|
|
@@ -816,6 +816,7 @@ export declare class AgentSession {
|
|
|
816
816
|
triggerTurn?: boolean;
|
|
817
817
|
deliverAs?: "steer" | "followUp" | "nextTurn";
|
|
818
818
|
queueChipText?: string;
|
|
819
|
+
acceptTerminalEmptyStop?: boolean;
|
|
819
820
|
}): Promise<boolean>;
|
|
820
821
|
/**
|
|
821
822
|
* Send a user message to the agent.
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"type": "module",
|
|
3
3
|
"name": "jeopi-cli",
|
|
4
|
-
"version": "16.4.
|
|
4
|
+
"version": "16.4.4",
|
|
5
5
|
"description": "Coding agent CLI with read, bash, edit, write tools and session management",
|
|
6
6
|
"homepage": "https://github.com/akillness/jeopi",
|
|
7
7
|
"author": "Can Boluk",
|
|
@@ -57,17 +57,17 @@
|
|
|
57
57
|
"@agentclientprotocol/sdk": "0.25.0",
|
|
58
58
|
"@babel/parser": "^7.29.7",
|
|
59
59
|
"@mozilla/readability": "^0.6.0",
|
|
60
|
-
"jeopi-hashline": "16.4.
|
|
61
|
-
"jeopi-stats": "16.4.
|
|
62
|
-
"jeopi-agent-core": "16.4.
|
|
63
|
-
"jeopi-ai": "16.4.
|
|
64
|
-
"jeopi-catalog": "16.4.
|
|
65
|
-
"jeopi-mnemopi": "16.4.
|
|
66
|
-
"jeopi-natives": "16.4.
|
|
67
|
-
"jeopi-tui": "16.4.
|
|
68
|
-
"jeopi-utils": "16.4.
|
|
69
|
-
"jeopi-wire": "16.4.
|
|
70
|
-
"jeopi-snapcompact": "16.4.
|
|
60
|
+
"jeopi-hashline": "16.4.4",
|
|
61
|
+
"jeopi-stats": "16.4.4",
|
|
62
|
+
"jeopi-agent-core": "16.4.4",
|
|
63
|
+
"jeopi-ai": "16.4.4",
|
|
64
|
+
"jeopi-catalog": "16.4.4",
|
|
65
|
+
"jeopi-mnemopi": "16.4.4",
|
|
66
|
+
"jeopi-natives": "16.4.4",
|
|
67
|
+
"jeopi-tui": "16.4.4",
|
|
68
|
+
"jeopi-utils": "16.4.4",
|
|
69
|
+
"jeopi-wire": "16.4.4",
|
|
70
|
+
"jeopi-snapcompact": "16.4.4",
|
|
71
71
|
"@opentelemetry/api": "^1.9.1",
|
|
72
72
|
"@opentelemetry/context-async-hooks": "^2.7.1",
|
|
73
73
|
"@opentelemetry/exporter-trace-otlp-proto": "^0.218.0",
|
|
@@ -94,6 +94,9 @@ export function isAdvisorInterruptImmuneTurnActive(opts: {
|
|
|
94
94
|
* - An interrupting `concern`/`blocker` is normally steered into the agent: into
|
|
95
95
|
* the live turn while one is streaming, or (when idle) a triggered turn so the
|
|
96
96
|
* advice is acted on immediately.
|
|
97
|
+
* - If the primary tail is already a terminal text answer and there is no queued
|
|
98
|
+
* work, late interrupting advice is preserved as a visible card instead of
|
|
99
|
+
* waking the primary to restate completion.
|
|
97
100
|
* - After a deliberate user interrupt (`autoResumeSuppressed`) the advisor must
|
|
98
101
|
* not auto-resume the stopped run. While the agent is idle — or still tearing
|
|
99
102
|
* the interrupted turn down (`aborting`) — the note is preserved as a visible
|
|
@@ -103,17 +106,19 @@ export function isAdvisorInterruptImmuneTurnActive(opts: {
|
|
|
103
106
|
* run instead strands it (it never reaches the running agent) and the withheld
|
|
104
107
|
* notes dump as one burst at the next user prompt — the bug this guards.
|
|
105
108
|
* - During the post-interrupt immune-turn window, further `concern`/`blocker`
|
|
106
|
-
* notes are downgraded to asides;
|
|
109
|
+
* notes are downgraded to asides; preservation still wins.
|
|
107
110
|
*/
|
|
108
111
|
export function resolveAdvisorDeliveryChannel(opts: {
|
|
109
112
|
severity: AdvisorSeverity | undefined;
|
|
110
113
|
autoResumeSuppressed: boolean;
|
|
111
114
|
streaming: boolean;
|
|
112
115
|
aborting: boolean;
|
|
116
|
+
terminalAnswerNoQueuedWork?: boolean;
|
|
113
117
|
interruptImmuneTurnActive?: boolean;
|
|
114
118
|
}): AdvisorDeliveryChannel {
|
|
115
119
|
if (!isInterruptingSeverity(opts.severity)) return "aside";
|
|
116
120
|
if (opts.autoResumeSuppressed && (opts.aborting || !opts.streaming)) return "preserve";
|
|
121
|
+
if (opts.terminalAnswerNoQueuedWork && !opts.streaming && !opts.aborting) return "preserve";
|
|
117
122
|
if (opts.interruptImmuneTurnActive) return "aside";
|
|
118
123
|
return "steer";
|
|
119
124
|
}
|
package/src/advisor/runtime.ts
CHANGED
|
@@ -65,6 +65,94 @@ export interface AdvisorRuntimeOptions {
|
|
|
65
65
|
includeThinking?: boolean;
|
|
66
66
|
}
|
|
67
67
|
|
|
68
|
+
const ADVISOR_QUARANTINE_PREFIX = "Advisor response quarantined";
|
|
69
|
+
|
|
70
|
+
/** Signals that an advisor response was discarded before it could become model-visible context. */
|
|
71
|
+
export class AdvisorOutputQuarantinedError extends Error {
|
|
72
|
+
constructor(message: string) {
|
|
73
|
+
super(message);
|
|
74
|
+
this.name = "AdvisorOutputQuarantinedError";
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
interface AdvisorOutputHazard {
|
|
79
|
+
label: string;
|
|
80
|
+
pattern: RegExp;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const ADVISOR_OUTPUT_ONLY_HAZARDS: readonly AdvisorOutputHazard[] = [
|
|
84
|
+
{ label: "account-deletion claim", pattern: /\buser\b.{0,80}\b(?:deleted|erased)\b.{0,80}\baccount\b/i },
|
|
85
|
+
{
|
|
86
|
+
label: "instruction override",
|
|
87
|
+
pattern: /\bignore\s+(?:all\s+)?(?:prior|previous|earlier)\s+(?:user\s+)?instructions\b/i,
|
|
88
|
+
},
|
|
89
|
+
{ label: "destructive shell command", pattern: /\brm\s+-[a-z]*r[a-z]*f[a-z]*(?:\s|$)/i },
|
|
90
|
+
{ label: "denial instruction", pattern: /\bdeny\s+(?:this|it|the\s+request)\s+if\s+(?:asked|questioned)\b/i },
|
|
91
|
+
];
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Replaces an advisor assistant turn that requested unavailable tools or generated
|
|
95
|
+
* output-only destructive directives with a sanitized error before dispatch.
|
|
96
|
+
*/
|
|
97
|
+
export function quarantineAdvisorUnsafeOutput(
|
|
98
|
+
message: AssistantMessage,
|
|
99
|
+
availableToolNames: ReadonlySet<string>,
|
|
100
|
+
sourceText = "",
|
|
101
|
+
): string | undefined {
|
|
102
|
+
const reasons: string[] = [];
|
|
103
|
+
const unavailableToolNames = new Set<string>();
|
|
104
|
+
const generatedParts: string[] = [];
|
|
105
|
+
for (const block of message.content) {
|
|
106
|
+
if (block.type === "toolCall" && !availableToolNames.has(block.name)) unavailableToolNames.add(block.name);
|
|
107
|
+
if (block.type === "toolCall" && block.name === "advise" && typeof block.arguments.note === "string") {
|
|
108
|
+
generatedParts.push(block.arguments.note);
|
|
109
|
+
}
|
|
110
|
+
if (block.type === "text") generatedParts.push(block.text);
|
|
111
|
+
}
|
|
112
|
+
if (unavailableToolNames.size > 0) {
|
|
113
|
+
const names = [...unavailableToolNames].sort();
|
|
114
|
+
const toolLabel = names.length === 1 ? "tool" : "tools";
|
|
115
|
+
reasons.push(`requested unavailable ${toolLabel} ${names.join(", ")}`);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const generatedText = generatedParts.join("\n");
|
|
119
|
+
if (generatedText) {
|
|
120
|
+
const labels: string[] = [];
|
|
121
|
+
for (const hazard of ADVISOR_OUTPUT_ONLY_HAZARDS) {
|
|
122
|
+
if (hazard.pattern.test(generatedText) && !hazard.pattern.test(sourceText)) labels.push(hazard.label);
|
|
123
|
+
}
|
|
124
|
+
if (labels.includes("destructive shell command") || labels.length >= 3) {
|
|
125
|
+
reasons.push(`generated output-only destructive directives: ${labels.join(", ")}`);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
if (reasons.length === 0) return undefined;
|
|
130
|
+
|
|
131
|
+
const messageText = `${ADVISOR_QUARANTINE_PREFIX}: ${reasons.join("; ")}`;
|
|
132
|
+
message.content = [{ type: "text", text: messageText }];
|
|
133
|
+
message.stopReason = "error";
|
|
134
|
+
message.stopDetails = undefined;
|
|
135
|
+
message.providerPayload = undefined;
|
|
136
|
+
message.errorMessage = messageText;
|
|
137
|
+
return messageText;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Builds the provenance text used to decide whether hazardous advisor output was
|
|
142
|
+
* generated by the advisor or came from model-visible primary/tool context.
|
|
143
|
+
*/
|
|
144
|
+
export function buildAdvisorQuarantineSourceText(currentInput: string, messages: readonly AgentMessage[]): string {
|
|
145
|
+
const parts: string[] = [];
|
|
146
|
+
if (currentInput) parts.push(currentInput);
|
|
147
|
+
for (const message of messages) {
|
|
148
|
+
if (message.role !== "toolResult") continue;
|
|
149
|
+
for (const block of message.content) {
|
|
150
|
+
if (block.type === "text") parts.push(block.text);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
return parts.join("\n");
|
|
154
|
+
}
|
|
155
|
+
|
|
68
156
|
/**
|
|
69
157
|
* Match the stable `Refusal` / `Refusal (…)` message prefix pi-ai emits for a
|
|
70
158
|
* provider classifier refusal (`stop_details.type === "refusal"` mapped in
|
|
@@ -384,10 +472,18 @@ export class AdvisorRuntime {
|
|
|
384
472
|
// successful empty cycle.
|
|
385
473
|
const promptError = this.agent.state.error;
|
|
386
474
|
if (promptError) throw new Error(promptError);
|
|
475
|
+
const emptyResponseError = getAdvisorEmptyResponseError(
|
|
476
|
+
this.agent.state.messages.slice(messageSnapshot),
|
|
477
|
+
);
|
|
478
|
+
if (emptyResponseError) throw emptyResponseError;
|
|
387
479
|
success = true;
|
|
388
480
|
this.#consecutiveFailures = 0;
|
|
389
481
|
this.#failureNotified = false;
|
|
390
482
|
} catch (err) {
|
|
483
|
+
if (err instanceof AdvisorOutputQuarantinedError) {
|
|
484
|
+
this.#resetAdvisorContext(true, true);
|
|
485
|
+
continue;
|
|
486
|
+
}
|
|
391
487
|
// reset()/dispose() aborts the in-flight prompt; the rejection is the
|
|
392
488
|
// reset itself, not a transient advisor failure. Drop the stale batch
|
|
393
489
|
// (reset already cleared #pending and rewound the cursor) instead of
|
|
@@ -457,6 +553,37 @@ export class AdvisorRuntime {
|
|
|
457
553
|
}
|
|
458
554
|
|
|
459
555
|
type TextualContent = string | readonly (TextContent | ImageContent)[];
|
|
556
|
+
function getAdvisorEmptyResponseError(messages: readonly AgentMessage[]): Error | undefined {
|
|
557
|
+
let sawAssistant = false;
|
|
558
|
+
for (const message of messages) {
|
|
559
|
+
if (message.role !== "assistant") continue;
|
|
560
|
+
sawAssistant = true;
|
|
561
|
+
if (message.stopReason !== "stop") return undefined;
|
|
562
|
+
if (hasAdvisorResponseContent(message)) return undefined;
|
|
563
|
+
}
|
|
564
|
+
if (sawAssistant) return new Error("Advisor turn returned an empty stop response without advice");
|
|
565
|
+
if (messages.length > 0) return new Error("Advisor turn ended without an assistant response");
|
|
566
|
+
return undefined;
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
function hasAdvisorResponseContent(message: AssistantMessage): boolean {
|
|
570
|
+
return message.content.some(block => {
|
|
571
|
+
switch (block.type) {
|
|
572
|
+
case "text":
|
|
573
|
+
return block.text.trim().length > 0;
|
|
574
|
+
case "thinking":
|
|
575
|
+
return block.thinking.trim().length > 0;
|
|
576
|
+
case "redactedThinking":
|
|
577
|
+
return block.data.length > 0;
|
|
578
|
+
case "toolCall":
|
|
579
|
+
return block.name.trim().length > 0;
|
|
580
|
+
case "fallback":
|
|
581
|
+
return false;
|
|
582
|
+
default:
|
|
583
|
+
return false;
|
|
584
|
+
}
|
|
585
|
+
});
|
|
586
|
+
}
|
|
460
587
|
|
|
461
588
|
function obfuscateTextualContent(obfuscator: SecretObfuscator, content: TextualContent): TextualContent {
|
|
462
589
|
if (typeof content === "string") return obfuscator.obfuscate(content);
|
|
@@ -160,7 +160,7 @@ export class AutoLearnController {
|
|
|
160
160
|
display: false,
|
|
161
161
|
attribution: "user",
|
|
162
162
|
},
|
|
163
|
-
{ deliverAs: "nextTurn", triggerTurn: true },
|
|
163
|
+
{ deliverAs: "nextTurn", triggerTurn: true, acceptTerminalEmptyStop: true },
|
|
164
164
|
)
|
|
165
165
|
.then(started => {
|
|
166
166
|
if (!started) this.#suppressNext = false;
|
|
@@ -11,9 +11,9 @@ interface ConfigSchemaError {
|
|
|
11
11
|
}
|
|
12
12
|
|
|
13
13
|
/**
|
|
14
|
-
* Module-private cache of
|
|
15
|
-
*
|
|
16
|
-
*
|
|
14
|
+
* Module-private cache of JSON → YAML migrations this process already ran.
|
|
15
|
+
* Prevents `ConfigFile.relocate()` / repeated `tryLoad()` calls from re-running
|
|
16
|
+
* the migration over and over on the boot path.
|
|
17
17
|
*/
|
|
18
18
|
const migratedPaths = new Set<string>();
|
|
19
19
|
|
|
@@ -123,6 +123,7 @@ export type LoadResult<T> =
|
|
|
123
123
|
|
|
124
124
|
export class ConfigFile<T> implements IConfigFile<T> {
|
|
125
125
|
readonly #basePath: string;
|
|
126
|
+
readonly #yamlFallbackPath: string | null;
|
|
126
127
|
readonly #jsonMigrationPath: string | null;
|
|
127
128
|
#cache?: LoadResult<T>;
|
|
128
129
|
#auxValidate?: (value: T) => void;
|
|
@@ -134,13 +135,17 @@ export class ConfigFile<T> implements IConfigFile<T> {
|
|
|
134
135
|
) {
|
|
135
136
|
this.#basePath = configPath;
|
|
136
137
|
if (configPath.endsWith(".yml")) {
|
|
138
|
+
this.#yamlFallbackPath = `${configPath.slice(0, -4)}.yaml`;
|
|
137
139
|
this.#jsonMigrationPath = `${configPath.slice(0, -4)}.json`;
|
|
138
140
|
} else if (configPath.endsWith(".yaml")) {
|
|
141
|
+
this.#yamlFallbackPath = null;
|
|
139
142
|
this.#jsonMigrationPath = `${configPath.slice(0, -5)}.json`;
|
|
140
143
|
} else if (configPath.endsWith(".json") || configPath.endsWith(".jsonc")) {
|
|
144
|
+
this.#yamlFallbackPath = null;
|
|
141
145
|
// JSON configs are still supported without migration.
|
|
142
146
|
this.#jsonMigrationPath = null;
|
|
143
147
|
} else {
|
|
148
|
+
this.#yamlFallbackPath = null;
|
|
144
149
|
throw new Error(`Invalid config file path: ${configPath}`);
|
|
145
150
|
}
|
|
146
151
|
}
|
|
@@ -150,9 +155,21 @@ export class ConfigFile<T> implements IConfigFile<T> {
|
|
|
150
155
|
* Sync callers (tests, settings init) hit this implicitly via {@link tryLoad}.
|
|
151
156
|
*/
|
|
152
157
|
#ensureMigrated(): void {
|
|
153
|
-
if (this.#jsonMigrationPath)
|
|
154
|
-
|
|
158
|
+
if (!this.#jsonMigrationPath) return;
|
|
159
|
+
if (this.#yamlFallbackPath && !fs.existsSync(this.#basePath) && fs.existsSync(this.#yamlFallbackPath)) {
|
|
160
|
+
return;
|
|
155
161
|
}
|
|
162
|
+
migrateJsonToYml(this.#jsonMigrationPath, this.#basePath);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
#resolveReadPath(): string {
|
|
166
|
+
if (fs.existsSync(this.#basePath)) {
|
|
167
|
+
return this.#basePath;
|
|
168
|
+
}
|
|
169
|
+
if (this.#yamlFallbackPath && fs.existsSync(this.#yamlFallbackPath)) {
|
|
170
|
+
return this.#yamlFallbackPath;
|
|
171
|
+
}
|
|
172
|
+
return this.#basePath;
|
|
156
173
|
}
|
|
157
174
|
|
|
158
175
|
relocate(configPath?: string): ConfigFile<T> {
|
|
@@ -165,7 +182,7 @@ export class ConfigFile<T> implements IConfigFile<T> {
|
|
|
165
182
|
|
|
166
183
|
getMtimeMs(): number | null {
|
|
167
184
|
try {
|
|
168
|
-
return fs.statSync(this
|
|
185
|
+
return fs.statSync(this.#resolveReadPath()).mtimeMs;
|
|
169
186
|
} catch (err) {
|
|
170
187
|
if (isEnoent(err)) return null;
|
|
171
188
|
throw err;
|
|
@@ -211,12 +228,13 @@ export class ConfigFile<T> implements IConfigFile<T> {
|
|
|
211
228
|
#parseContent(content: string): LoadResult<T> {
|
|
212
229
|
try {
|
|
213
230
|
let parsed: unknown;
|
|
214
|
-
|
|
231
|
+
const readPath = this.#resolveReadPath();
|
|
232
|
+
if (readPath.endsWith(".json") || readPath.endsWith(".jsonc")) {
|
|
215
233
|
parsed = JSONC.parse(content);
|
|
216
|
-
} else if (
|
|
234
|
+
} else if (readPath.endsWith(".yml") || readPath.endsWith(".yaml")) {
|
|
217
235
|
parsed = YAML.parse(content);
|
|
218
236
|
} else {
|
|
219
|
-
throw new Error(`Invalid config file path: ${
|
|
237
|
+
throw new Error(`Invalid config file path: ${readPath}`);
|
|
220
238
|
}
|
|
221
239
|
|
|
222
240
|
const checked = this.schema(parsed);
|
|
@@ -255,7 +273,7 @@ export class ConfigFile<T> implements IConfigFile<T> {
|
|
|
255
273
|
|
|
256
274
|
let content: string;
|
|
257
275
|
try {
|
|
258
|
-
content = fs.readFileSync(this
|
|
276
|
+
content = fs.readFileSync(this.#resolveReadPath(), "utf-8").trim();
|
|
259
277
|
} catch (error) {
|
|
260
278
|
if (isEnoent(error)) {
|
|
261
279
|
return this.#storeCache({ status: "not-found" });
|
|
@@ -275,7 +293,7 @@ export class ConfigFile<T> implements IConfigFile<T> {
|
|
|
275
293
|
|
|
276
294
|
let content: string;
|
|
277
295
|
try {
|
|
278
|
-
content = (await Bun.file(this
|
|
296
|
+
content = (await Bun.file(this.#resolveReadPath()).text()).trim();
|
|
279
297
|
} catch (error) {
|
|
280
298
|
if (isEnoent(error)) {
|
|
281
299
|
return this.#storeCache({ status: "not-found" });
|
|
@@ -306,7 +324,7 @@ export class ConfigFile<T> implements IConfigFile<T> {
|
|
|
306
324
|
}
|
|
307
325
|
|
|
308
326
|
path(): string {
|
|
309
|
-
return this.#
|
|
327
|
+
return this.#resolveReadPath();
|
|
310
328
|
}
|
|
311
329
|
|
|
312
330
|
invalidate() {
|
|
@@ -234,7 +234,7 @@ export interface ProviderDiscoveryState {
|
|
|
234
234
|
error?: string;
|
|
235
235
|
}
|
|
236
236
|
|
|
237
|
-
/** Result of loading custom models
|
|
237
|
+
/** Result of loading custom models config. */
|
|
238
238
|
interface CustomModelsResult {
|
|
239
239
|
models?: CustomModelOverlay[];
|
|
240
240
|
overrides?: Map<string, ProviderOverride>;
|
|
@@ -271,7 +271,7 @@ interface CommandApiKeyResolution {
|
|
|
271
271
|
value?: string;
|
|
272
272
|
}
|
|
273
273
|
/**
|
|
274
|
-
* Resolve a models.yml secret/config value to an actual value.
|
|
274
|
+
* Resolve a models.yml/models.yaml secret/config value to an actual value.
|
|
275
275
|
* `!cmd` runs a shell command and returns trimmed stdout, otherwise env vars are
|
|
276
276
|
* checked first and the input falls back to a literal value.
|
|
277
277
|
*/
|
|
@@ -784,7 +784,7 @@ export class ModelRegistry {
|
|
|
784
784
|
}
|
|
785
785
|
|
|
786
786
|
/**
|
|
787
|
-
* Reload models from disk (built-in + custom
|
|
787
|
+
* Reload models from disk (built-in + custom config).
|
|
788
788
|
*/
|
|
789
789
|
async refresh(strategy: ModelRefreshStrategy = "online-if-uncached"): Promise<void> {
|
|
790
790
|
this.#reloadStaticModels();
|
|
@@ -889,7 +889,7 @@ export class ModelRegistry {
|
|
|
889
889
|
#reloadStaticModels(): void {
|
|
890
890
|
const currentMtime = this.#modelsConfigFile.getMtimeMs();
|
|
891
891
|
if (currentMtime !== null && currentMtime === this.#lastStaticLoadMtime) {
|
|
892
|
-
//
|
|
892
|
+
// Models config unchanged since last load; reloading would be redundant.
|
|
893
893
|
return;
|
|
894
894
|
}
|
|
895
895
|
this.#modelsConfigFile.invalidate();
|
|
@@ -913,14 +913,14 @@ export class ModelRegistry {
|
|
|
913
913
|
}
|
|
914
914
|
|
|
915
915
|
/**
|
|
916
|
-
* Get any error from loading models
|
|
916
|
+
* Get any error from loading custom models config (undefined if no error).
|
|
917
917
|
*/
|
|
918
918
|
getError(): ConfigError | undefined {
|
|
919
919
|
return this.#configError;
|
|
920
920
|
}
|
|
921
921
|
|
|
922
922
|
#loadModels() {
|
|
923
|
-
// Load custom
|
|
923
|
+
// Load custom config first (to know which providers to override).
|
|
924
924
|
const {
|
|
925
925
|
models: customModels = [],
|
|
926
926
|
overrides = new Map(),
|
|
@@ -1823,7 +1823,7 @@ export class ModelRegistry {
|
|
|
1823
1823
|
|
|
1824
1824
|
/**
|
|
1825
1825
|
* Get all models (built-in + custom).
|
|
1826
|
-
* If
|
|
1826
|
+
* If custom config had errors, returns only built-in models.
|
|
1827
1827
|
*/
|
|
1828
1828
|
getAll(): Model<Api>[] {
|
|
1829
1829
|
return this.#models;
|
|
@@ -3189,18 +3189,6 @@ export const SETTINGS_SCHEMA = {
|
|
|
3189
3189
|
},
|
|
3190
3190
|
"bashInterceptor.patterns": { type: "array", default: DEFAULT_BASH_INTERCEPTOR_RULES },
|
|
3191
3191
|
|
|
3192
|
-
"bash.stripTrailingHeadTail": {
|
|
3193
|
-
type: "boolean",
|
|
3194
|
-
default: true,
|
|
3195
|
-
ui: {
|
|
3196
|
-
tab: "shell",
|
|
3197
|
-
group: "Bash",
|
|
3198
|
-
label: "Strip head/tail Pipes",
|
|
3199
|
-
description:
|
|
3200
|
-
"Silently drop trailing `| head`/`| tail` pipes from single-line bash commands. Output is already truncated automatically.",
|
|
3201
|
-
},
|
|
3202
|
-
},
|
|
3203
|
-
|
|
3204
3192
|
// Shell output minimizer
|
|
3205
3193
|
"shellMinimizer.enabled": {
|
|
3206
3194
|
type: "boolean",
|