@oh-my-pi/pi-coding-agent 17.3.5 → 17.3.8
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 +80 -0
- package/dist/{CHANGELOG-tt9k4jpr.md → CHANGELOG-vr9cckb4.md} +80 -0
- package/dist/cli.js +2993 -3001
- package/dist/docs-index.generated.txt +1 -1
- package/dist/{tool-views.generated-jdfmzwmn.js → tool-views.generated-dd2km5r2.js} +19 -19
- package/dist/types/advisor/advise-tool.d.ts +4 -2
- package/dist/types/cli/auth-broker-cli.d.ts +15 -0
- package/dist/types/cli/stats-cli.d.ts +1 -6
- package/dist/types/cli/update-cli.d.ts +8 -0
- package/dist/types/cli-commands.d.ts +10 -2
- package/dist/types/commands/stats.d.ts +4 -0
- package/dist/types/config/settings-schema.d.ts +28 -0
- package/dist/types/config/settings.d.ts +9 -0
- package/dist/types/extensibility/extensions/runner.d.ts +23 -4
- package/dist/types/extensibility/extensions/types.d.ts +58 -0
- package/dist/types/launch/presence.d.ts +4 -1
- package/dist/types/mcp/oauth-credentials.d.ts +23 -0
- package/dist/types/mcp/oauth-flow.d.ts +11 -0
- package/dist/types/mnemopi/backend.d.ts +12 -0
- package/dist/types/modes/components/tool-execution.d.ts +12 -0
- package/dist/types/modes/controllers/input-controller.d.ts +2 -0
- package/dist/types/modes/interactive-mode.d.ts +25 -3
- package/dist/types/modes/types.d.ts +10 -0
- package/dist/types/session/agent-session.d.ts +3 -0
- package/dist/types/session/prewalk.d.ts +4 -0
- package/dist/types/session/session-entries.d.ts +0 -1
- package/dist/types/session/session-manager.d.ts +12 -0
- package/dist/types/session/session-stats.d.ts +13 -1
- package/dist/types/session/skill-title-input.d.ts +13 -0
- package/dist/types/slash-commands/helpers/stats-dashboard.d.ts +1 -0
- package/dist/types/subprocess/worker-client.d.ts +7 -4
- package/dist/types/task/label.d.ts +2 -0
- package/dist/types/task/render.d.ts +2 -0
- package/dist/types/tiny/completion-prompt.d.ts +2 -0
- package/dist/types/tiny/title-client.d.ts +6 -4
- package/dist/types/tiny/title-protocol.d.ts +1 -0
- package/dist/types/tiny/worker.d.ts +27 -0
- package/dist/types/tools/bash.d.ts +1 -1
- package/dist/types/tools/file-write-fallback.d.ts +124 -0
- package/dist/types/tools/index.d.ts +1 -0
- package/dist/types/tools/path-utils.d.ts +23 -0
- package/dist/types/tools/read-format.d.ts +6 -0
- package/dist/types/tools/read-summary.d.ts +7 -1
- package/dist/types/utils/block-context.d.ts +14 -0
- package/dist/types/utils/fetch-timeout.d.ts +15 -0
- package/dist/types/utils/git.d.ts +25 -1
- package/dist/types/web/search/providers/tinyfish.d.ts +4 -0
- package/package.json +13 -13
- package/src/advisor/advise-tool.ts +5 -3
- package/src/cli/auth-broker-cli.ts +36 -1
- package/src/cli/profile-bootstrap.ts +2 -6
- package/src/cli/stats-cli.ts +6 -72
- package/src/cli/update-cli.ts +63 -11
- package/src/cli-commands.ts +61 -7
- package/src/commands/completions.ts +2 -1
- package/src/commands/stats.ts +7 -4
- package/src/commit/agentic/index.ts +15 -2
- package/src/commit/git/diff.ts +6 -2
- package/src/config/model-resolver.ts +52 -6
- package/src/config/models-config.ts +2 -2
- package/src/config/settings-schema.ts +33 -0
- package/src/config/settings.ts +159 -30
- package/src/discovery/helpers.ts +45 -2
- package/src/discovery/omp-plugins.ts +2 -1
- package/src/discovery/opencode.ts +56 -3
- package/src/edit/hashline/filesystem.ts +9 -3
- package/src/edit/modes/patch.ts +31 -5
- package/src/eval/js/process-entry.ts +4 -4
- package/src/export/html/tool-views.generated.js +19 -19
- package/src/extensibility/extensions/loader.ts +11 -0
- package/src/extensibility/extensions/runner.ts +118 -5
- package/src/extensibility/extensions/types.ts +60 -0
- package/src/extensibility/extensions/wrapper.ts +10 -1
- package/src/extensibility/plugins/legacy-pi-compat.ts +47 -0
- package/src/launch/client.ts +9 -4
- package/src/launch/presence.ts +19 -4
- package/src/lsp/defaults.json +1 -1
- package/src/lsp/writethrough.ts +20 -10
- package/src/mcp/manager.ts +41 -20
- package/src/mcp/oauth-credentials.ts +38 -0
- package/src/mcp/oauth-flow.ts +21 -0
- package/src/mcp/tool-bridge.ts +32 -16
- package/src/mnemopi/backend.ts +35 -3
- package/src/modes/components/model-hub.ts +37 -4
- package/src/modes/components/settings-selector.ts +17 -11
- package/src/modes/components/tool-execution.ts +97 -29
- package/src/modes/components/tree-selector.ts +7 -2
- package/src/modes/controllers/event-controller.ts +12 -2
- package/src/modes/controllers/input-controller.ts +64 -27
- package/src/modes/controllers/mcp-command-controller.ts +13 -4
- package/src/modes/interactive-mode.ts +79 -11
- package/src/modes/types.ts +11 -0
- package/src/prompts/system/memory-extraction-system.md +5 -22
- package/src/prompts/system/system-prompt.md +1 -1
- package/src/session/agent-session.ts +52 -6
- package/src/session/messages.ts +6 -0
- package/src/session/prewalk.ts +25 -7
- package/src/session/session-entries.ts +0 -1
- package/src/session/session-maintenance.ts +10 -1
- package/src/session/session-manager.ts +15 -0
- package/src/session/session-stats.ts +24 -3
- package/src/session/settings-stream-fn.ts +7 -0
- package/src/session/skill-title-input.ts +32 -0
- package/src/session/turn-recovery.ts +23 -18
- package/src/slash-commands/builtin-session.ts +1 -1
- package/src/slash-commands/helpers/stats-dashboard.ts +23 -9
- package/src/subprocess/worker-client.ts +8 -5
- package/src/task/executor.ts +11 -0
- package/src/task/index.ts +2 -0
- package/src/task/label.ts +14 -1
- package/src/task/persisted-revive.ts +13 -0
- package/src/task/render.ts +1 -1
- package/src/task/structured-subagent.ts +5 -2
- package/src/tiny/completion-prompt.ts +16 -0
- package/src/tiny/title-client.ts +15 -6
- package/src/tiny/title-protocol.ts +8 -1
- package/src/tiny/worker.ts +21 -19
- package/src/tools/bash.ts +7 -1
- package/src/tools/file-write-fallback.ts +467 -0
- package/src/tools/index.ts +1 -0
- package/src/tools/path-utils.ts +79 -0
- package/src/tools/read-format.ts +16 -2
- package/src/tools/read-summary.ts +9 -4
- package/src/tools/read.ts +306 -72
- package/src/utils/block-context.ts +15 -1
- package/src/utils/fetch-timeout.ts +33 -0
- package/src/utils/git.ts +54 -11
- package/src/web/search/providers/browser-page.ts +21 -3
- package/src/web/search/providers/tinyfish.ts +26 -0
|
@@ -1950,6 +1950,21 @@ export class SessionManager {
|
|
|
1950
1950
|
return this.#sessionFile;
|
|
1951
1951
|
}
|
|
1952
1952
|
|
|
1953
|
+
/**
|
|
1954
|
+
* Whether the current session has actually been materialized to durable
|
|
1955
|
+
* storage (the JSONL exists on disk / in the active storage backend).
|
|
1956
|
+
*
|
|
1957
|
+
* Session persistence is lazy: the file is only written once the history
|
|
1958
|
+
* contains an assistant message (or an explicit {@link ensureOnDisk}
|
|
1959
|
+
* caller forces it). Until then {@link getSessionFile} returns an allocated
|
|
1960
|
+
* path that leads nowhere, so a `--resume <id>` hint built from it would
|
|
1961
|
+
* always fail. Consumers that advertise a resume command must gate on this
|
|
1962
|
+
* (issue #8860).
|
|
1963
|
+
*/
|
|
1964
|
+
isSessionOnDisk(): boolean {
|
|
1965
|
+
return !!this.#sessionFile && this.#storage.existsSync(this.#sessionFile);
|
|
1966
|
+
}
|
|
1967
|
+
|
|
1953
1968
|
getArtifactsDir(): string | null {
|
|
1954
1969
|
if (this.#adoptedArtifactManager) return this.#adoptedArtifactManager.dir;
|
|
1955
1970
|
return artifactsDirectoryFor(this.#sessionFile);
|
|
@@ -22,6 +22,12 @@ interface PendingContextSnapshot {
|
|
|
22
22
|
promptTokens: number;
|
|
23
23
|
nonMessageTokens: number;
|
|
24
24
|
cutoffCount: number;
|
|
25
|
+
/**
|
|
26
|
+
* Compaction epoch at rebase time. Distinguishes a genuinely fresh in-turn
|
|
27
|
+
* anchor (same epoch) from a post-cutoff anchor that predates a mid-run
|
|
28
|
+
* compaction (older epoch) so the latter never out-ranks this snapshot.
|
|
29
|
+
*/
|
|
30
|
+
epoch: number;
|
|
25
31
|
}
|
|
26
32
|
|
|
27
33
|
/** Capabilities the stats tracker borrows from its owning session. */
|
|
@@ -44,6 +50,7 @@ export class SessionStatsTracker {
|
|
|
44
50
|
readonly #host: SessionStatsTrackerHost;
|
|
45
51
|
#pendingContextSnapshot: PendingContextSnapshot | undefined;
|
|
46
52
|
#contextUsageRevision = 0;
|
|
53
|
+
#compactionEpoch = 0;
|
|
47
54
|
|
|
48
55
|
constructor(host: SessionStatsTrackerHost) {
|
|
49
56
|
this.#host = host;
|
|
@@ -162,8 +169,11 @@ export class SessionStatsTracker {
|
|
|
162
169
|
}
|
|
163
170
|
}
|
|
164
171
|
|
|
172
|
+
const anchorEpoch = anchorAssistant?.contextSnapshot?.compactionEpoch ?? 0;
|
|
165
173
|
const useAnchor =
|
|
166
|
-
anchorAssistant !== undefined &&
|
|
174
|
+
anchorAssistant !== undefined &&
|
|
175
|
+
anchorIndex !== -1 &&
|
|
176
|
+
(!pending || (anchorIndex >= pending.cutoffCount && anchorEpoch >= pending.epoch));
|
|
167
177
|
if (useAnchor && anchorAssistant) {
|
|
168
178
|
const promptTokens = correctedPromptTokens(anchorAssistant);
|
|
169
179
|
const nonMessageTokens =
|
|
@@ -255,6 +265,15 @@ export class SessionStatsTracker {
|
|
|
255
265
|
return this.#contextUsageRevision;
|
|
256
266
|
}
|
|
257
267
|
|
|
268
|
+
/**
|
|
269
|
+
* Monotonic compaction epoch, bumped whenever history is compacted. Stamped
|
|
270
|
+
* onto each assistant snapshot at record time so {@link getContextBreakdown}
|
|
271
|
+
* can reject a post-cutoff anchor whose usage predates the last compaction.
|
|
272
|
+
*/
|
|
273
|
+
get compactionEpoch(): number {
|
|
274
|
+
return this.#compactionEpoch;
|
|
275
|
+
}
|
|
276
|
+
|
|
258
277
|
/** Non-message token count captured for the active provider request. */
|
|
259
278
|
get pendingNonMessageTokens(): number | undefined {
|
|
260
279
|
return this.#pendingContextSnapshot?.nonMessageTokens;
|
|
@@ -292,6 +311,7 @@ export class SessionStatsTracker {
|
|
|
292
311
|
assistant.contextSnapshot = {
|
|
293
312
|
promptTokens: calculatePromptTokens(assistant.usage),
|
|
294
313
|
nonMessageTokens: computeNonMessageTokens(this.#host.session),
|
|
314
|
+
compactionEpoch: this.#compactionEpoch,
|
|
295
315
|
};
|
|
296
316
|
}
|
|
297
317
|
const snapshot = assistant.contextSnapshot;
|
|
@@ -302,13 +322,14 @@ export class SessionStatsTracker {
|
|
|
302
322
|
}
|
|
303
323
|
|
|
304
324
|
/** Sets or clears the in-flight context snapshot. */
|
|
305
|
-
setPendingSnapshot(snapshot: PendingContextSnapshot | undefined): void {
|
|
306
|
-
this.#pendingContextSnapshot = snapshot;
|
|
325
|
+
setPendingSnapshot(snapshot: Omit<PendingContextSnapshot, "epoch"> | undefined): void {
|
|
326
|
+
this.#pendingContextSnapshot = snapshot ? { ...snapshot, epoch: this.#compactionEpoch } : undefined;
|
|
307
327
|
this.#contextUsageRevision++;
|
|
308
328
|
}
|
|
309
329
|
|
|
310
330
|
/** Recomputes an in-flight snapshot after history is compacted or rewritten. */
|
|
311
331
|
rebaseAfterCompaction(): void {
|
|
332
|
+
this.#compactionEpoch++;
|
|
312
333
|
if (!this.#pendingContextSnapshot) return;
|
|
313
334
|
const nonMessageTokens = computeNonMessageTokens(this.#host.session);
|
|
314
335
|
const messages = this.#host.agent.state.messages;
|
|
@@ -41,6 +41,12 @@ export function createSettingsAwareStreamFn(settings: Settings, base: StreamFn =
|
|
|
41
41
|
: model.api === "openai-responses"
|
|
42
42
|
? settings.get("textVerbosity")
|
|
43
43
|
: undefined;
|
|
44
|
+
// "auto" leaves the option unset so provider defaults and the
|
|
45
|
+
// PI_CACHE_RETENTION env override keep working; anything else is an
|
|
46
|
+
// explicit per-request retention (long restores 1h Anthropic TTLs and
|
|
47
|
+
// implicitly disables the short-entry keep-alive refresh loop).
|
|
48
|
+
const cacheRetentionSetting = settings.get("providers.cacheRetention");
|
|
49
|
+
const cacheRetention = cacheRetentionSetting === "auto" ? undefined : cacheRetentionSetting;
|
|
44
50
|
const streamFirstEventTimeoutMs = timeoutSecondsToMs(settings.get("providers.streamFirstEventTimeoutSeconds"));
|
|
45
51
|
const streamIdleTimeoutMs = timeoutSecondsToMs(settings.get("providers.streamIdleTimeoutSeconds"));
|
|
46
52
|
// Server-side fallback (opt-in): when the user enables it AND the
|
|
@@ -60,6 +66,7 @@ export function createSettingsAwareStreamFn(settings: Settings, base: StreamFn =
|
|
|
60
66
|
openrouterVariant: streamOptions?.openrouterVariant ?? openrouterVariant,
|
|
61
67
|
antigravityEndpointMode: streamOptions?.antigravityEndpointMode ?? antigravityEndpointMode,
|
|
62
68
|
textVerbosity: streamOptions?.textVerbosity ?? textVerbosity,
|
|
69
|
+
cacheRetention: streamOptions?.cacheRetention ?? cacheRetention,
|
|
63
70
|
streamFirstEventTimeoutMs: streamOptions?.streamFirstEventTimeoutMs ?? streamFirstEventTimeoutMs,
|
|
64
71
|
streamIdleTimeoutMs: streamOptions?.streamIdleTimeoutMs ?? streamIdleTimeoutMs,
|
|
65
72
|
maxRetryDelayMs: streamOptions?.maxRetryDelayMs ?? settings.get("retry.maxDelayMs"),
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/** Compact title-model input for a user-invoked `/skill:<name>` prompt. */
|
|
2
|
+
export function skillPromptTitleInput(input: { name?: string; args?: string; queueChipText?: string }): string {
|
|
3
|
+
const chip = input.queueChipText?.trim();
|
|
4
|
+
if (chip) return chip;
|
|
5
|
+
const name = input.name?.trim();
|
|
6
|
+
const args = input.args?.trim();
|
|
7
|
+
if (name && args) return `/skill:${name} ${args}`;
|
|
8
|
+
if (name) return `/skill:${name}`;
|
|
9
|
+
return args ?? "";
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/** Title text for a persisted skill-prompt custom message. Never the expanded SKILL.md body. */
|
|
13
|
+
export function titleTextFromSkillPrompt(message: {
|
|
14
|
+
role: string;
|
|
15
|
+
customType?: string;
|
|
16
|
+
attribution?: string;
|
|
17
|
+
details?: unknown;
|
|
18
|
+
}): string | undefined {
|
|
19
|
+
if (message.role !== "custom" || message.customType !== "skill-prompt" || message.attribution !== "user") {
|
|
20
|
+
return undefined;
|
|
21
|
+
}
|
|
22
|
+
let name: string | undefined;
|
|
23
|
+
let args: string | undefined;
|
|
24
|
+
let queueChipText: string | undefined;
|
|
25
|
+
if (message.details && typeof message.details === "object") {
|
|
26
|
+
const details = message.details as Record<string, unknown>;
|
|
27
|
+
if (typeof details.name === "string") name = details.name;
|
|
28
|
+
if (typeof details.args === "string") args = details.args;
|
|
29
|
+
if (typeof details.__queueChipText === "string") queueChipText = details.__queueChipText;
|
|
30
|
+
}
|
|
31
|
+
return skillPromptTitleInput({ name, args, queueChipText }) || undefined;
|
|
32
|
+
}
|
|
@@ -20,7 +20,6 @@ import type {
|
|
|
20
20
|
} from "@oh-my-pi/pi-ai";
|
|
21
21
|
import { calculateRateLimitBackoffMs, parseRateLimitReason } from "@oh-my-pi/pi-ai";
|
|
22
22
|
import * as AIError from "@oh-my-pi/pi-ai/error";
|
|
23
|
-
import { kCursorExecResolved } from "@oh-my-pi/pi-ai/utils/block-symbols";
|
|
24
23
|
import { isFireworksFastModelId, toFireworksBaseModelId } from "@oh-my-pi/pi-catalog/fireworks-model-id";
|
|
25
24
|
import { modelsAreEqual } from "@oh-my-pi/pi-catalog/models";
|
|
26
25
|
import { extractRetryHint, logger, prompt } from "@oh-my-pi/pi-utils";
|
|
@@ -1177,25 +1176,15 @@ export class TurnRecovery {
|
|
|
1177
1176
|
if (!reasonlessAbort && !streamStall && !transportReset) return undefined;
|
|
1178
1177
|
if (reasonlessAbort && genericAbort) message.errorId = AIError.create(AIError.Flag.Abort);
|
|
1179
1178
|
|
|
1180
|
-
//
|
|
1181
|
-
//
|
|
1182
|
-
//
|
|
1183
|
-
//
|
|
1184
|
-
//
|
|
1185
|
-
// the
|
|
1186
|
-
// irrelevant. An HTTP/2 RST_STREAM / NGHTTP2_* close also ends the Connect
|
|
1187
|
-
// stream, so there is no in-flight server exec to race — unmarked MCP/todo
|
|
1188
|
-
// blocks are safe to continue once every emitted call has a result.
|
|
1179
|
+
// Idle stall and HTTP/2 RST both close the Cursor Connect stream:
|
|
1180
|
+
// the lazy watchdog aborts the request signal, and cursor.ts then
|
|
1181
|
+
// calls `h2Request.close()`. There is no in-flight server exec to
|
|
1182
|
+
// race, so unmarked MCP/todo blocks can continue once every emitted
|
|
1183
|
+
// call has a matching result. A reasonless abort ends the turn and
|
|
1184
|
+
// the agent loop pairs leftover calls with `executed: false`.
|
|
1189
1185
|
const resolvedToolCallIds: string[] = [];
|
|
1190
1186
|
for (const block of message.content) {
|
|
1191
1187
|
if (block.type !== "toolCall") continue;
|
|
1192
|
-
if (
|
|
1193
|
-
streamStall &&
|
|
1194
|
-
message.provider === "cursor" &&
|
|
1195
|
-
(!(kCursorExecResolved in block) || block[kCursorExecResolved] !== true)
|
|
1196
|
-
) {
|
|
1197
|
-
return undefined;
|
|
1198
|
-
}
|
|
1199
1188
|
resolvedToolCallIds.push(block.id);
|
|
1200
1189
|
}
|
|
1201
1190
|
if (resolvedToolCallIds.length === 0) return undefined;
|
|
@@ -1660,6 +1649,9 @@ export class TurnRecovery {
|
|
|
1660
1649
|
if (AIError.isContextOverflow(message, model.contextWindow ?? 0)) return false;
|
|
1661
1650
|
if (AIError.is(id, AIError.Flag.UsageLimit)) return false;
|
|
1662
1651
|
if (AIError.is(id, AIError.Flag.AuthFailed)) return false;
|
|
1652
|
+
// A thinking loop is a same-model resample signal, not a router fault, so a
|
|
1653
|
+
// base-model swap would abandon the loop-guard redirect (issue #8760).
|
|
1654
|
+
if (AIError.is(id, AIError.Flag.ThinkingLoop)) return false;
|
|
1663
1655
|
return this.#host.modelRegistry.find("fireworks", toFireworksBaseModelId(model.id)) !== undefined;
|
|
1664
1656
|
}
|
|
1665
1657
|
|
|
@@ -1969,10 +1961,23 @@ export class TurnRecovery {
|
|
|
1969
1961
|
);
|
|
1970
1962
|
if (switchedCredential) delayMs = 0;
|
|
1971
1963
|
}
|
|
1964
|
+
// A thinking-loop abort is not a provider failure — it is the loop guard
|
|
1965
|
+
// asking for a same-model resample, paired with a hidden
|
|
1966
|
+
// `thinking-loop-redirect` notice that only makes sense on the model that
|
|
1967
|
+
// looped. Walking `fallbackChains` (or parking the selector on a cooldown)
|
|
1968
|
+
// would swap a healthy planning turn to another family based on chain
|
|
1969
|
+
// contents, not model health (issue #8760). Keep it on the same model; the
|
|
1970
|
+
// retry budget still bounds a genuinely stuck stream.
|
|
1971
|
+
const thinkingLoop = AIError.is(id, AIError.Flag.ThinkingLoop);
|
|
1972
1972
|
if (!staleOpenAIResponsesReplayError && !switchedCredential && currentSelector) {
|
|
1973
1973
|
// A refusal chain stops at the retry budget: the exhausted-attempt
|
|
1974
1974
|
// last resort is for provider failures, not classifier decisions.
|
|
1975
|
-
if (
|
|
1975
|
+
if (
|
|
1976
|
+
allowModelFallback &&
|
|
1977
|
+
retrySettings.modelFallback &&
|
|
1978
|
+
!thinkingLoop &&
|
|
1979
|
+
!(retryBudgetExhausted && classifierRefusal)
|
|
1980
|
+
) {
|
|
1976
1981
|
if (!classifierRefusal) {
|
|
1977
1982
|
this.noteRetryFallbackCooldown(currentSelector, parsedRetryAfterMs, errorMessage);
|
|
1978
1983
|
}
|
|
@@ -336,7 +336,7 @@ export const BUILTIN_SESSION_SLASH_COMMANDS: ReadonlyArray<SlashCommandSpec> = [
|
|
|
336
336
|
{
|
|
337
337
|
name: "stats",
|
|
338
338
|
description: "Launch the local stats dashboard",
|
|
339
|
-
inlineHint: "[--port <port>]",
|
|
339
|
+
inlineHint: "[--port <port>] [--host <host>]",
|
|
340
340
|
allowArgs: true,
|
|
341
341
|
handle: async (command, runtime) => {
|
|
342
342
|
const parsed = parseStatsDashboardArgs(command.args);
|
|
@@ -11,6 +11,7 @@ interface StatsDashboardServer {
|
|
|
11
11
|
|
|
12
12
|
export interface StatsDashboardArgs {
|
|
13
13
|
port: number;
|
|
14
|
+
host: string;
|
|
14
15
|
}
|
|
15
16
|
|
|
16
17
|
export interface StatsDashboardLaunchResult {
|
|
@@ -20,7 +21,7 @@ export interface StatsDashboardLaunchResult {
|
|
|
20
21
|
|
|
21
22
|
let activeStatsServer: StatsDashboardServer | undefined;
|
|
22
23
|
|
|
23
|
-
const STATS_DASHBOARD_USAGE = "Usage: /stats [--port <port>]";
|
|
24
|
+
const STATS_DASHBOARD_USAGE = "Usage: /stats [--port <port>] [--host <host>]";
|
|
24
25
|
|
|
25
26
|
function parsePort(value: string | undefined): number | string {
|
|
26
27
|
if (!value) return `Missing port. ${STATS_DASHBOARD_USAGE}`;
|
|
@@ -33,6 +34,7 @@ function parsePort(value: string | undefined): number | string {
|
|
|
33
34
|
export function parseStatsDashboardArgs(args: string): StatsDashboardArgs | { error: string } {
|
|
34
35
|
const tokens = args.split(/\s+/).filter(Boolean);
|
|
35
36
|
let port = DEFAULT_STATS_DASHBOARD_PORT;
|
|
37
|
+
let host = "127.0.0.1";
|
|
36
38
|
|
|
37
39
|
for (let i = 0; i < tokens.length; i++) {
|
|
38
40
|
const token = tokens[i];
|
|
@@ -48,28 +50,40 @@ export function parseStatsDashboardArgs(args: string): StatsDashboardArgs | { er
|
|
|
48
50
|
port = parsed;
|
|
49
51
|
continue;
|
|
50
52
|
}
|
|
53
|
+
if (token === "--host") {
|
|
54
|
+
const value = tokens[++i];
|
|
55
|
+
if (!value) return { error: `Missing host. ${STATS_DASHBOARD_USAGE}` };
|
|
56
|
+
host = value;
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
if (token.startsWith("--host=")) {
|
|
60
|
+
const value = token.slice("--host=".length);
|
|
61
|
+
if (!value) return { error: `Missing host. ${STATS_DASHBOARD_USAGE}` };
|
|
62
|
+
host = value;
|
|
63
|
+
continue;
|
|
64
|
+
}
|
|
51
65
|
return { error: `Unknown option: ${token}. ${STATS_DASHBOARD_USAGE}` };
|
|
52
66
|
}
|
|
53
67
|
|
|
54
|
-
return { port };
|
|
68
|
+
return { port, host };
|
|
55
69
|
}
|
|
56
70
|
|
|
57
71
|
export async function launchStatsDashboard(args: StatsDashboardArgs): Promise<StatsDashboardLaunchResult> {
|
|
58
72
|
const { processed, files } = await stats.syncAllSessions();
|
|
59
73
|
const total = await stats.getTotalMessageCount();
|
|
60
|
-
let
|
|
74
|
+
let requestedAddressIgnored = false;
|
|
61
75
|
|
|
62
76
|
if (!activeStatsServer) {
|
|
63
|
-
activeStatsServer = await stats.startServer(args.port);
|
|
64
|
-
} else if (args.port !== activeStatsServer.port) {
|
|
65
|
-
|
|
77
|
+
activeStatsServer = await stats.startServer(args.port, args.host);
|
|
78
|
+
} else if (args.port !== activeStatsServer.port || args.host !== activeStatsServer.hostname) {
|
|
79
|
+
requestedAddressIgnored = true;
|
|
66
80
|
}
|
|
67
81
|
|
|
68
|
-
const url =
|
|
82
|
+
const url = stats.formatStatsDashboardUrl(activeStatsServer.hostname, activeStatsServer.port);
|
|
69
83
|
openUtils.openPath(url);
|
|
70
84
|
|
|
71
|
-
const serverLine =
|
|
72
|
-
? `Dashboard already running at: ${url} (requested
|
|
85
|
+
const serverLine = requestedAddressIgnored
|
|
86
|
+
? `Dashboard already running at: ${url} (requested ${args.host}:${args.port} ignored)`
|
|
73
87
|
: `Dashboard available at: ${url}`;
|
|
74
88
|
|
|
75
89
|
return {
|
|
@@ -108,17 +108,20 @@ export const SMOKE_TEST_TIMEOUT_MS = 30_000;
|
|
|
108
108
|
/**
|
|
109
109
|
* Resolve the command used to relaunch the agent CLI into worker mode. In a
|
|
110
110
|
* compiled binary the entry point is the binary itself; otherwise re-enter the
|
|
111
|
-
* declared worker-host entry
|
|
112
|
-
*
|
|
113
|
-
*
|
|
114
|
-
*
|
|
111
|
+
* declared worker-host entry by absolute path. Workers deliberately spawn
|
|
112
|
+
* without a pinned cwd there: they share the parent's foreground process
|
|
113
|
+
* group, and terminal cwd heuristics (kitty's new_tab_with_cwd) read the
|
|
114
|
+
* newest process in that group, so anchoring them to the install dir leaks
|
|
115
|
+
* into newly opened terminal tabs. With no declared host entry (bun test, SDK
|
|
116
|
+
* embedding) fall back to a cwd-relative `src/cli.ts`, which Bun subprocess
|
|
117
|
+
* IPC handles more reliably under `bun test`.
|
|
115
118
|
*/
|
|
116
119
|
export function resolveWorkerSpawnCmd(workerArg: string): WorkerSpawnCommand {
|
|
117
120
|
const executable = stripWindowsExtendedLengthPathPrefix(process.execPath);
|
|
118
121
|
if (isCompiledBinary()) return { cmd: [executable, workerArg] };
|
|
119
122
|
const hostEntry = workerHostEntry();
|
|
120
123
|
if (hostEntry) {
|
|
121
|
-
return { cmd: [executable,
|
|
124
|
+
return { cmd: [executable, hostEntry, workerArg] };
|
|
122
125
|
}
|
|
123
126
|
const packageRoot = path.resolve(import.meta.dir, "..", "..");
|
|
124
127
|
return { cmd: [executable, "src/cli.ts", workerArg], cwd: packageRoot };
|
package/src/task/executor.ts
CHANGED
|
@@ -35,6 +35,7 @@ import type { HindsightSessionState } from "../hindsight/state";
|
|
|
35
35
|
import type { LocalProtocolOptions } from "../internal-urls";
|
|
36
36
|
import type { MCPManager } from "../mcp/manager";
|
|
37
37
|
import type { MnemopiSessionState } from "../mnemopi/state";
|
|
38
|
+
import { initializeExtensions } from "../modes/runtime-init";
|
|
38
39
|
import subagentAsyncPendingTemplate from "../prompts/system/subagent-async-pending.md" with { type: "text" };
|
|
39
40
|
import subagentSystemPromptTemplate from "../prompts/system/subagent-system-prompt.md" with { type: "text" };
|
|
40
41
|
import submitReminderTemplate from "../prompts/system/subagent-yield-reminder.md" with { type: "text" };
|
|
@@ -3167,6 +3168,16 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
|
|
|
3167
3168
|
const { session: revived } = await createAgentSession(
|
|
3168
3169
|
buildSubagentSessionOptions(reopened, expectedAgentRef),
|
|
3169
3170
|
);
|
|
3171
|
+
// Re-run the executor's extension wiring on the rebuilt session.
|
|
3172
|
+
// Skipping it leaves the runner pre-init, so a `tool_call` handler
|
|
3173
|
+
// touching a runtime action trips the fail-closed gate and blocks
|
|
3174
|
+
// every tool (including `yield`) in the revived agent (issue #8824).
|
|
3175
|
+
await initializeExtensions(revived, {
|
|
3176
|
+
reportSendError: (action, err) =>
|
|
3177
|
+
logger.error("Extension send failed", { action, error: err.message }),
|
|
3178
|
+
reportRuntimeError: err =>
|
|
3179
|
+
logger.error("Extension error", { path: err.extensionPath, error: err.error }),
|
|
3180
|
+
});
|
|
3170
3181
|
AgentRegistry.global().syncSessionStatus(id, revived);
|
|
3171
3182
|
installIrcWakeTurnMonitor(revived);
|
|
3172
3183
|
return revived;
|
package/src/task/index.ts
CHANGED
|
@@ -1424,6 +1424,8 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
|
|
|
1424
1424
|
...(Object.hasOwn(params, "outputSchema") ? { outputSchema: params.outputSchema } : {}),
|
|
1425
1425
|
...(Object.hasOwn(params, "schemaMode") ? { schemaMode: params.schemaMode } : {}),
|
|
1426
1426
|
...(params.effort !== undefined ? { effort: params.effort } : {}),
|
|
1427
|
+
// `name` is the spawn handle: keep it for id allocation when this
|
|
1428
|
+
// path did not pre-reserve one. Do not treat it as a HUD description.
|
|
1427
1429
|
identity: { id: preAllocatedId, label: params.name },
|
|
1428
1430
|
index: spawnIndex,
|
|
1429
1431
|
parentToolCallId: toolCallId,
|
package/src/task/label.ts
CHANGED
|
@@ -9,6 +9,17 @@ import { generateSessionTitle } from "../utils/title-generator";
|
|
|
9
9
|
|
|
10
10
|
const TASK_LABEL_SYSTEM_PROMPT = prompt.render(taskLabelSystemPrompt);
|
|
11
11
|
|
|
12
|
+
/** True when a generated label is just the spawn handle, including `Name-2`. */
|
|
13
|
+
export function labelEchoesHandle(handle: string | undefined, label: string): boolean {
|
|
14
|
+
if (!handle) return false;
|
|
15
|
+
if (label.localeCompare(handle, undefined, { sensitivity: "accent" }) === 0) return true;
|
|
16
|
+
const separator = handle.lastIndexOf("-");
|
|
17
|
+
if (separator <= 0) return false;
|
|
18
|
+
const prefix = handle.slice(0, separator);
|
|
19
|
+
const suffix = handle.slice(separator + 1);
|
|
20
|
+
return /^\d+$/.test(suffix) && prefix.localeCompare(label, undefined, { sensitivity: "accent" }) === 0;
|
|
21
|
+
}
|
|
22
|
+
|
|
12
23
|
/** Compresses a delegated assignment into a one-sentence UI label via the tiny title model — fired by the executor spawn path because the task wire schema no longer carries a `description`; null on empty input or failure. */
|
|
13
24
|
export async function generateTaskLabel(
|
|
14
25
|
assignment: string,
|
|
@@ -20,7 +31,7 @@ export async function generateTaskLabel(
|
|
|
20
31
|
const text = assignment.trim();
|
|
21
32
|
if (!text) return null;
|
|
22
33
|
try {
|
|
23
|
-
|
|
34
|
+
const label = await generateSessionTitle(
|
|
24
35
|
text,
|
|
25
36
|
registry,
|
|
26
37
|
settings,
|
|
@@ -30,6 +41,8 @@ export async function generateTaskLabel(
|
|
|
30
41
|
TASK_LABEL_SYSTEM_PROMPT,
|
|
31
42
|
signal,
|
|
32
43
|
);
|
|
44
|
+
if (!label || labelEchoesHandle(sessionId, label)) return null;
|
|
45
|
+
return label;
|
|
33
46
|
} catch (err) {
|
|
34
47
|
logger.debug("task-label: generation failed", {
|
|
35
48
|
sessionId,
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import * as fs from "node:fs/promises";
|
|
2
|
+
import { logger } from "@oh-my-pi/pi-utils";
|
|
2
3
|
import type { ModelRegistry } from "../config/model-registry";
|
|
3
4
|
import { formatModelRoleAlias } from "../config/model-roles";
|
|
4
5
|
import type { Settings } from "../config/settings";
|
|
5
6
|
import { MCPManager } from "../mcp/manager";
|
|
7
|
+
import { initializeExtensions } from "../modes/runtime-init";
|
|
6
8
|
import type { PersistedSubagentReviverFactory } from "../registry/agent-lifecycle";
|
|
7
9
|
import { AgentRegistry, MAIN_AGENT_ID } from "../registry/agent-registry";
|
|
8
10
|
import { createAgentSession } from "../sdk";
|
|
@@ -153,6 +155,17 @@ export function createPersistedSubagentReviverFactory(
|
|
|
153
155
|
// `alwaysInclude` can re-add non-defaultInactive extension/custom tools
|
|
154
156
|
// the original run didn't carry. Unknown/missing names are ignored.
|
|
155
157
|
await session.setActiveToolsByName([...init.tools, ...session.getMountedXdevToolNames()]);
|
|
158
|
+
// Wire the extension runtime exactly as the live executor does. Without
|
|
159
|
+
// this the runner stays pre-init, every action method throws
|
|
160
|
+
// `ExtensionRuntimeNotInitializedError`, and a `tool_call` handler that
|
|
161
|
+
// touches a runtime action trips the fail-closed gate in `emitToolCall`,
|
|
162
|
+
// blocking every tool — including the hidden `yield` — in the revived
|
|
163
|
+
// agent. `session_start` also re-runs so extensions restore per-session
|
|
164
|
+
// state (issue #8824).
|
|
165
|
+
await initializeExtensions(session, {
|
|
166
|
+
reportSendError: (action, err) => logger.error("Extension send failed", { action, error: err.message }),
|
|
167
|
+
reportRuntimeError: err => logger.error("Extension error", { path: err.extensionPath, error: err.error }),
|
|
168
|
+
});
|
|
156
169
|
// Cold revives must drive registry status themselves — createAgentSession
|
|
157
170
|
// doesn't wire this generically (the live path does it in the executor).
|
|
158
171
|
// The internal run-state signal precedes deferrable public `agent_end`,
|
package/src/task/render.ts
CHANGED
|
@@ -710,7 +710,7 @@ function formatAgentHeaderLabel(args: Partial<TaskParams> | undefined): string |
|
|
|
710
710
|
}
|
|
711
711
|
|
|
712
712
|
/** Dim `⟨agent⟩` badge for a non-default agent type; empty for the generic worker. */
|
|
713
|
-
function agentTypeBadge(agent: string | undefined, theme: Theme): string {
|
|
713
|
+
export function agentTypeBadge(agent: string | undefined, theme: Theme): string {
|
|
714
714
|
const trimmed = agent?.trim();
|
|
715
715
|
if (!trimmed || trimmed === "task") return "";
|
|
716
716
|
return ` ${theme.fg("dim", `${theme.format.bracketLeft}${trimmed}${theme.format.bracketRight}`)}`;
|
|
@@ -245,6 +245,7 @@ function assertDepthAndSpawnAllowed(request: StructuredSubagentRequest, agentNam
|
|
|
245
245
|
export async function resolveEffectiveSubagentPolicy(
|
|
246
246
|
request: StructuredSubagentRequest,
|
|
247
247
|
): Promise<EffectiveSubagentPolicy> {
|
|
248
|
+
await request.session.settings.reloadFromDisk();
|
|
248
249
|
const spawnPolicy = resolveSpawnPolicy(request.session.getSessionSpawns());
|
|
249
250
|
const agentName = request.agent?.trim() || spawnPolicy.defaultAgent;
|
|
250
251
|
const planMode = request.session.getPlanModeState?.()?.enabled === true;
|
|
@@ -393,7 +394,9 @@ function buildExecutorOptions(
|
|
|
393
394
|
assignment: request.assignment.trim(),
|
|
394
395
|
context: request.context?.trim() || undefined,
|
|
395
396
|
planReference: undefined,
|
|
396
|
-
|
|
397
|
+
// Task `name` is the spawn handle (id allocation). Eval `label` is a
|
|
398
|
+
// real UI description. Copy it only for eval so generateTaskLabel can run.
|
|
399
|
+
description: request.invocationKind === "eval" ? trimToUndefined(request.identity?.label) : undefined,
|
|
397
400
|
index: request.index ?? 0,
|
|
398
401
|
parentToolCallId: request.parentToolCallId,
|
|
399
402
|
detached: request.detached,
|
|
@@ -476,7 +479,7 @@ function buildFailureResult(
|
|
|
476
479
|
agentSource: policy.agent.source,
|
|
477
480
|
task: renderSubagentPrompt(request.assignment),
|
|
478
481
|
assignment: request.assignment.trim(),
|
|
479
|
-
description: trimToUndefined(request.identity?.label),
|
|
482
|
+
description: request.invocationKind === "eval" ? trimToUndefined(request.identity?.label) : undefined,
|
|
480
483
|
exitCode: 1,
|
|
481
484
|
output: "",
|
|
482
485
|
stderr: message,
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { TextGenerationPipeline } from "@huggingface/transformers";
|
|
2
|
+
|
|
3
|
+
export function buildCompletionPrompt(
|
|
4
|
+
tokenizer: TextGenerationPipeline["tokenizer"],
|
|
5
|
+
promptText: string,
|
|
6
|
+
systemPrompt?: string,
|
|
7
|
+
): string {
|
|
8
|
+
const userMessage = { role: "user", content: promptText };
|
|
9
|
+
const chat = systemPrompt?.trim() ? [{ role: "system", content: systemPrompt.trim() }, userMessage] : [userMessage];
|
|
10
|
+
const chatTemplateOptions = {
|
|
11
|
+
add_generation_prompt: true,
|
|
12
|
+
tokenize: false,
|
|
13
|
+
enable_thinking: false,
|
|
14
|
+
};
|
|
15
|
+
return `${tokenizer.apply_chat_template(chat, chatTemplateOptions)}`;
|
|
16
|
+
}
|
package/src/tiny/title-client.ts
CHANGED
|
@@ -52,6 +52,12 @@ export interface TinyTitleGenerateOptions {
|
|
|
52
52
|
systemPrompt?: string;
|
|
53
53
|
}
|
|
54
54
|
|
|
55
|
+
export interface TinyModelCompletionOptions {
|
|
56
|
+
maxTokens?: number;
|
|
57
|
+
signal?: AbortSignal;
|
|
58
|
+
systemPrompt?: string;
|
|
59
|
+
}
|
|
60
|
+
|
|
55
61
|
function normalizeTinyTitleGenerateOptions(
|
|
56
62
|
options: AbortSignal | TinyTitleGenerateOptions | undefined,
|
|
57
63
|
): TinyTitleGenerateOptions {
|
|
@@ -260,11 +266,7 @@ export class TinyTitleClient {
|
|
|
260
266
|
}
|
|
261
267
|
}
|
|
262
268
|
|
|
263
|
-
async complete(
|
|
264
|
-
modelKey: string,
|
|
265
|
-
prompt: string,
|
|
266
|
-
options: { maxTokens?: number; signal?: AbortSignal } = {},
|
|
267
|
-
): Promise<string | null> {
|
|
269
|
+
async complete(modelKey: string, prompt: string, options: TinyModelCompletionOptions = {}): Promise<string | null> {
|
|
268
270
|
if (!isTinyMemoryLocalModelKey(modelKey)) return null;
|
|
269
271
|
if (options.signal?.aborted || this.#failedModels.has(modelKey)) return null;
|
|
270
272
|
|
|
@@ -281,7 +283,14 @@ export class TinyTitleClient {
|
|
|
281
283
|
};
|
|
282
284
|
options.signal?.addEventListener("abort", abort, { once: true });
|
|
283
285
|
try {
|
|
284
|
-
worker.send({
|
|
286
|
+
worker.send({
|
|
287
|
+
type: "complete",
|
|
288
|
+
id,
|
|
289
|
+
modelKey,
|
|
290
|
+
prompt,
|
|
291
|
+
maxTokens: options.maxTokens,
|
|
292
|
+
systemPrompt: options.systemPrompt,
|
|
293
|
+
});
|
|
285
294
|
return await promise;
|
|
286
295
|
} finally {
|
|
287
296
|
options.signal?.removeEventListener("abort", abort);
|
|
@@ -30,7 +30,14 @@ export interface TinyTitleProgressEvent {
|
|
|
30
30
|
export type TinyTitleWorkerInbound =
|
|
31
31
|
| { type: "ping"; id: string }
|
|
32
32
|
| { type: "generate"; id: string; modelKey: TinyTitleLocalModelKey; message: string; systemPrompt?: string }
|
|
33
|
-
| {
|
|
33
|
+
| {
|
|
34
|
+
type: "complete";
|
|
35
|
+
id: string;
|
|
36
|
+
modelKey: TinyLocalModelKey;
|
|
37
|
+
prompt: string;
|
|
38
|
+
maxTokens?: number;
|
|
39
|
+
systemPrompt?: string;
|
|
40
|
+
}
|
|
34
41
|
| { type: "download"; id: string; modelKey: TinyLocalModelKey };
|
|
35
42
|
|
|
36
43
|
export type TinyTitleWorkerOutbound =
|
package/src/tiny/worker.ts
CHANGED
|
@@ -19,6 +19,7 @@ import {
|
|
|
19
19
|
sendProgress,
|
|
20
20
|
type TransformersRuntimeMetadata,
|
|
21
21
|
} from "../subprocess/worker-runtime";
|
|
22
|
+
import { buildCompletionPrompt } from "./completion-prompt";
|
|
22
23
|
import { resolveTinyModelDevicePreference, type TinyModelDevice, tinyModelDeviceLoadOrder } from "./device";
|
|
23
24
|
import { resolveTinyModelDtypeOverride, type TinyModelDtype } from "./dtype";
|
|
24
25
|
import { formatTitleUserMessage } from "./message-preproc";
|
|
@@ -42,7 +43,7 @@ const TINY_TITLE_SYSTEM_PROMPT = prompt.render(titleSystemPrompt);
|
|
|
42
43
|
const tinyModelDevicePreference = resolveTinyModelDevicePreference();
|
|
43
44
|
const tinyModelDtypeOverride = resolveTinyModelDtypeOverride();
|
|
44
45
|
|
|
45
|
-
interface TransformersRuntime extends TransformersRuntimeMetadata {
|
|
46
|
+
export interface TransformersRuntime extends TransformersRuntimeMetadata {
|
|
46
47
|
env: {
|
|
47
48
|
cacheDir?: string;
|
|
48
49
|
allowLocalModels?: boolean;
|
|
@@ -79,7 +80,13 @@ function getTinyTitleRuntimeDir(): string {
|
|
|
79
80
|
);
|
|
80
81
|
}
|
|
81
82
|
|
|
82
|
-
|
|
83
|
+
/** Stops generation at the first occurrence of `text` in the *generated* tokens.
|
|
84
|
+
*
|
|
85
|
+
* The window must be anchored to the generation boundary, not to the end of the
|
|
86
|
+
* whole sequence: a prompt that itself contains the stop string (chat-level
|
|
87
|
+
* few-shot examples ending in `</title>`, for instance) would otherwise match on
|
|
88
|
+
* prompt tokens and stop before the model emits anything. */
|
|
89
|
+
export function createStopOnTextCriteria(
|
|
83
90
|
transformers: TransformersRuntime,
|
|
84
91
|
tokenizer: TextGenerationPipeline["tokenizer"],
|
|
85
92
|
text: string,
|
|
@@ -87,6 +94,8 @@ function createStopOnTextCriteria(
|
|
|
87
94
|
class StopOnTextCriteria extends transformers.StoppingCriteria {
|
|
88
95
|
#tokenizer: TextGenerationPipeline["tokenizer"];
|
|
89
96
|
#text: string;
|
|
97
|
+
/** First generated index per batch entry, captured on the first call. */
|
|
98
|
+
#generatedStarts: number[] = [];
|
|
90
99
|
|
|
91
100
|
constructor() {
|
|
92
101
|
super();
|
|
@@ -95,8 +104,10 @@ function createStopOnTextCriteria(
|
|
|
95
104
|
}
|
|
96
105
|
|
|
97
106
|
override _call(inputIds: number[][]): boolean[] {
|
|
98
|
-
return inputIds.map(ids => {
|
|
99
|
-
const
|
|
107
|
+
return inputIds.map((ids, index) => {
|
|
108
|
+
const generatedStart = this.#generatedStarts[index] ?? Math.max(0, ids.length - 1);
|
|
109
|
+
this.#generatedStarts[index] = generatedStart;
|
|
110
|
+
const tail = ids.slice(Math.max(generatedStart, ids.length - STOP_DECODE_WINDOW_TOKENS));
|
|
100
111
|
const decoded = this.#tokenizer.decode(tail, {
|
|
101
112
|
skip_special_tokens: false,
|
|
102
113
|
clean_up_tokenization_spaces: false,
|
|
@@ -265,21 +276,10 @@ async function generateTitle(
|
|
|
265
276
|
return extractTinyTitle(output[0]?.generated_text ?? "", message);
|
|
266
277
|
}
|
|
267
278
|
|
|
268
|
-
function buildCompletionPrompt(generator: TextGenerationPipeline, promptText: string): string {
|
|
269
|
-
const chat = [{ role: "user", content: promptText }];
|
|
270
|
-
const chatTemplateOptions = {
|
|
271
|
-
add_generation_prompt: true,
|
|
272
|
-
tokenize: false,
|
|
273
|
-
enable_thinking: false,
|
|
274
|
-
};
|
|
275
|
-
return `${generator.tokenizer.apply_chat_template(chat, chatTemplateOptions)}`;
|
|
276
|
-
}
|
|
277
|
-
|
|
278
279
|
/**
|
|
279
|
-
*
|
|
280
|
-
* and
|
|
281
|
-
*
|
|
282
|
-
* caller's own parser. Output is capped to keep local inference latency bounded.
|
|
280
|
+
* Completion path for Mnemopi memory tasks. Extraction can carry a dedicated
|
|
281
|
+
* system prompt and user payload; consolidation retains the generic user-only
|
|
282
|
+
* prompt. Output is capped to keep local inference latency bounded.
|
|
283
283
|
*/
|
|
284
284
|
async function generateCompletion(
|
|
285
285
|
transport: TinyTitleTransport,
|
|
@@ -287,9 +287,10 @@ async function generateCompletion(
|
|
|
287
287
|
modelKey: TinyLocalModelKey,
|
|
288
288
|
promptText: string,
|
|
289
289
|
maxTokens: number | undefined,
|
|
290
|
+
systemPrompt: string | undefined,
|
|
290
291
|
): Promise<string | null> {
|
|
291
292
|
const generator = await loadPipeline(modelKey, transport, requestId);
|
|
292
|
-
const text = buildCompletionPrompt(generator, promptText);
|
|
293
|
+
const text = buildCompletionPrompt(generator.tokenizer, promptText, systemPrompt);
|
|
293
294
|
const requested = maxTokens ?? MEMORY_COMPLETION_DEFAULT_MAX_NEW_TOKENS;
|
|
294
295
|
const maxNewTokens = Math.min(Math.max(1, requested), COMPLETION_MAX_NEW_TOKENS);
|
|
295
296
|
const output = (await generator(text, {
|
|
@@ -332,6 +333,7 @@ async function handleQueuedRequest(
|
|
|
332
333
|
request.modelKey,
|
|
333
334
|
request.prompt,
|
|
334
335
|
request.maxTokens,
|
|
336
|
+
request.systemPrompt,
|
|
335
337
|
);
|
|
336
338
|
transport.send({ type: "completion", id: request.id, text });
|
|
337
339
|
return;
|