@oh-my-pi/pi-coding-agent 16.1.0 → 16.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +36 -1
- package/dist/cli.js +3134 -3158
- package/dist/types/cli/bench-cli.d.ts +2 -1
- package/dist/types/config/settings-schema.d.ts +28 -37
- package/dist/types/lsp/types.d.ts +5 -3
- package/dist/types/main.d.ts +2 -0
- package/dist/types/modes/components/assistant-message.d.ts +12 -0
- package/dist/types/modes/components/cache-invalidation-marker.d.ts +7 -2
- package/dist/types/modes/components/welcome.d.ts +1 -1
- package/dist/types/sdk.d.ts +19 -2
- package/dist/types/session/auth-broker-config.d.ts +33 -6
- package/dist/types/system-prompt.d.ts +5 -1
- package/dist/types/task/executor.d.ts +10 -0
- package/dist/types/tools/find.d.ts +0 -2
- package/dist/types/tools/search.d.ts +3 -3
- package/package.json +12 -12
- package/scripts/measure-prompt-tokens.ts +63 -0
- package/src/cli/bench-cli.ts +64 -3
- package/src/cli/startup-cwd.ts +3 -13
- package/src/config/settings-schema.ts +34 -37
- package/src/config/settings.ts +40 -0
- package/src/cursor.ts +1 -1
- package/src/debug/raw-sse-buffer.ts +31 -10
- package/src/eval/py/prelude.py +1 -1
- package/src/export/html/tool-views.generated.js +1 -1
- package/src/extensibility/extensions/runner.ts +8 -2
- package/src/internal-urls/docs-index.generated.txt +1 -1
- package/src/lsp/client.ts +9 -9
- package/src/lsp/types.ts +6 -3
- package/src/main.ts +29 -9
- package/src/modes/components/assistant-message.ts +86 -0
- package/src/modes/components/cache-invalidation-marker.ts +12 -2
- package/src/modes/components/settings-defs.ts +7 -0
- package/src/modes/components/tips.txt +2 -1
- package/src/modes/components/welcome.ts +86 -8
- package/src/modes/controllers/event-controller.ts +1 -1
- package/src/prompts/system/personalities/default.md +8 -16
- package/src/prompts/system/system-prompt.md +101 -115
- package/src/prompts/tools/ast-edit.md +10 -12
- package/src/prompts/tools/ast-grep.md +14 -18
- package/src/prompts/tools/bash.md +19 -21
- package/src/prompts/tools/browser.md +24 -24
- package/src/prompts/tools/checkpoint.md +0 -1
- package/src/prompts/tools/debug.md +11 -15
- package/src/prompts/tools/eval.md +27 -27
- package/src/prompts/tools/find.md +6 -10
- package/src/prompts/tools/github.md +11 -15
- package/src/prompts/tools/goal.md +0 -7
- package/src/prompts/tools/inspect-image.md +0 -1
- package/src/prompts/tools/irc.md +15 -24
- package/src/prompts/tools/job.md +5 -8
- package/src/prompts/tools/learn.md +2 -2
- package/src/prompts/tools/lsp.md +27 -30
- package/src/prompts/tools/manage-skill.md +4 -4
- package/src/prompts/tools/read.md +21 -23
- package/src/prompts/tools/replace.md +0 -1
- package/src/prompts/tools/resolve.md +4 -9
- package/src/prompts/tools/rewind.md +1 -1
- package/src/prompts/tools/search.md +8 -10
- package/src/prompts/tools/task.md +33 -38
- package/src/prompts/tools/todo.md +14 -18
- package/src/prompts/tools/web-search.md +0 -4
- package/src/prompts/tools/write.md +1 -1
- package/src/sdk.ts +49 -102
- package/src/session/agent-session.ts +23 -12
- package/src/session/auth-broker-config.ts +36 -76
- package/src/session/session-history-format.ts +1 -1
- package/src/session/session-manager.ts +33 -6
- package/src/system-prompt.ts +28 -8
- package/src/task/executor.ts +57 -0
- package/src/task/index.ts +15 -1
- package/src/tools/browser.ts +1 -1
- package/src/tools/eval.ts +1 -1
- package/src/tools/find.ts +4 -17
- package/src/tools/memory-edit.ts +1 -1
- package/src/tools/search.ts +5 -5
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Resolve auth-broker connection configuration for the local omp client.
|
|
3
3
|
*
|
|
4
|
+
* This is a thin coding-agent wrapper around the shared resolver in
|
|
5
|
+
* `@oh-my-pi/pi-ai/auth-broker/discover` that preserves the process-lifetime
|
|
6
|
+
* memoization expected by the CLI and injects the full `resolveConfigValue`
|
|
7
|
+
* (including `!command` config indirection) from coding-agent's config layer.
|
|
8
|
+
*
|
|
4
9
|
* Precedence (highest first):
|
|
5
10
|
* 1. `OMP_AUTH_BROKER_URL` / `OMP_AUTH_BROKER_TOKEN` env vars.
|
|
6
11
|
* 2. `auth.broker.url` / `auth.broker.token` in `~/.omp/agent/config.yml`
|
|
@@ -15,55 +20,18 @@
|
|
|
15
20
|
* `runRootCommand`, and we want hand-edited config entries to be honoured at
|
|
16
21
|
* boot without forcing a startup reorder.
|
|
17
22
|
*/
|
|
18
|
-
import * as path from "node:path";
|
|
19
|
-
import { getAgentDir, getConfigRootDir, isEnoent, logger } from "@oh-my-pi/pi-utils";
|
|
20
|
-
import { YAML } from "bun";
|
|
21
|
-
import { resolveConfigValue } from "../config/resolve-config-value";
|
|
22
|
-
|
|
23
|
-
export interface AuthBrokerClientConfig {
|
|
24
|
-
url: string;
|
|
25
|
-
token: string;
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
/** Path to the local bearer token file. Created on the broker host by `omp auth-broker token`. */
|
|
29
|
-
export function getAuthBrokerTokenFilePath(): string {
|
|
30
|
-
return path.join(getConfigRootDir(), "auth-broker.token");
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
async function readTokenFile(): Promise<string | null> {
|
|
34
|
-
try {
|
|
35
|
-
const raw = await Bun.file(getAuthBrokerTokenFilePath()).text();
|
|
36
|
-
const trimmed = raw.trim();
|
|
37
|
-
return trimmed.length > 0 ? trimmed : null;
|
|
38
|
-
} catch (err) {
|
|
39
|
-
if (isEnoent(err)) return null;
|
|
40
|
-
logger.warn("auth-broker token file unreadable", { error: String(err) });
|
|
41
|
-
return null;
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
23
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
24
|
+
import {
|
|
25
|
+
type AuthBrokerClientConfig,
|
|
26
|
+
type DiscoverAuthStorageOptions,
|
|
27
|
+
discoverAuthStorage as discoverAuthStorageShared,
|
|
28
|
+
getAuthBrokerTokenFilePath,
|
|
29
|
+
resolveAuthBrokerConfig as resolveAuthBrokerConfigShared,
|
|
30
|
+
} from "@oh-my-pi/pi-ai/auth-broker/discover";
|
|
31
|
+
import { getAgentDir } from "@oh-my-pi/pi-utils";
|
|
32
|
+
import { resolveConfigValue } from "../config/resolve-config-value";
|
|
49
33
|
|
|
50
|
-
|
|
51
|
-
const configPath = path.join(getAgentDir(), "config.yml");
|
|
52
|
-
try {
|
|
53
|
-
const raw = await Bun.file(configPath).text();
|
|
54
|
-
const parsed = YAML.parse(raw);
|
|
55
|
-
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return {};
|
|
56
|
-
const record = parsed as Record<string, unknown>;
|
|
57
|
-
const url = typeof record["auth.broker.url"] === "string" ? (record["auth.broker.url"] as string) : undefined;
|
|
58
|
-
const token =
|
|
59
|
-
typeof record["auth.broker.token"] === "string" ? (record["auth.broker.token"] as string) : undefined;
|
|
60
|
-
return { url, token };
|
|
61
|
-
} catch (err) {
|
|
62
|
-
if (isEnoent(err)) return {};
|
|
63
|
-
logger.warn("auth-broker config.yml unreadable", { error: String(err) });
|
|
64
|
-
return {};
|
|
65
|
-
}
|
|
66
|
-
}
|
|
34
|
+
export { type AuthBrokerClientConfig, getAuthBrokerTokenFilePath };
|
|
67
35
|
|
|
68
36
|
/**
|
|
69
37
|
* Process-lifetime memo for {@link resolveAuthBrokerConfig}. Keyed on the env
|
|
@@ -88,7 +56,10 @@ let cachedConfigPromise: Promise<AuthBrokerClientConfig | null> | null = null;
|
|
|
88
56
|
export function resolveAuthBrokerConfig(): Promise<AuthBrokerClientConfig | null> {
|
|
89
57
|
const key = `${process.env.OMP_AUTH_BROKER_URL ?? ""}\u0000${process.env.OMP_AUTH_BROKER_TOKEN ?? ""}\u0000${getAgentDir()}`;
|
|
90
58
|
if (cachedConfigPromise && cachedConfigKey === key) return cachedConfigPromise;
|
|
91
|
-
const promise =
|
|
59
|
+
const promise = resolveAuthBrokerConfigShared({
|
|
60
|
+
agentDir: getAgentDir(),
|
|
61
|
+
configValueResolver: resolveConfigValue,
|
|
62
|
+
});
|
|
92
63
|
cachedConfigKey = key;
|
|
93
64
|
cachedConfigPromise = promise;
|
|
94
65
|
promise.catch(() => {
|
|
@@ -100,32 +71,21 @@ export function resolveAuthBrokerConfig(): Promise<AuthBrokerClientConfig | null
|
|
|
100
71
|
return promise;
|
|
101
72
|
}
|
|
102
73
|
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
}
|
|
120
|
-
if (!url) return null;
|
|
121
|
-
|
|
122
|
-
const token =
|
|
123
|
-
(envToken && envToken.length > 0 ? envToken : undefined) ?? configToken ?? (await readTokenFile()) ?? undefined;
|
|
124
|
-
if (!token) {
|
|
125
|
-
throw new Error(
|
|
126
|
-
`OMP_AUTH_BROKER_URL is set (${url}) but no bearer token is available. ` +
|
|
127
|
-
`Set OMP_AUTH_BROKER_TOKEN, the \`auth.broker.token\` config entry, or place one at ${getAuthBrokerTokenFilePath()}.`,
|
|
128
|
-
);
|
|
129
|
-
}
|
|
130
|
-
return { url, token };
|
|
74
|
+
/**
|
|
75
|
+
* Create an AuthStorage instance, using the broker when configured and falling
|
|
76
|
+
* back to the local SQLite store otherwise. Delegates to the shared resolver in
|
|
77
|
+
* pi-ai so the CLI, subagents, and the catalog generator all see the same
|
|
78
|
+
* credentials.
|
|
79
|
+
*
|
|
80
|
+
* Default `agentDir` is the current configured agent directory.
|
|
81
|
+
*/
|
|
82
|
+
export function discoverAuthStorage(
|
|
83
|
+
agentDir: string = getAgentDir(),
|
|
84
|
+
options?: Omit<DiscoverAuthStorageOptions, "agentDir" | "configValueResolver">,
|
|
85
|
+
): ReturnType<typeof discoverAuthStorageShared> {
|
|
86
|
+
return discoverAuthStorageShared({
|
|
87
|
+
...options,
|
|
88
|
+
agentDir,
|
|
89
|
+
configValueResolver: resolveConfigValue,
|
|
90
|
+
});
|
|
131
91
|
}
|
|
@@ -102,7 +102,7 @@ function primaryArg(name: string, args: Record<string, unknown> | undefined): st
|
|
|
102
102
|
rest[key] = value;
|
|
103
103
|
restCount++;
|
|
104
104
|
}
|
|
105
|
-
if (restCount === 0) return "";
|
|
105
|
+
if (restCount === 0) return "{}";
|
|
106
106
|
try {
|
|
107
107
|
return oneLine(JSON.stringify(rest));
|
|
108
108
|
} catch {
|
|
@@ -1,7 +1,15 @@
|
|
|
1
1
|
import * as fs from "node:fs";
|
|
2
2
|
import * as path from "node:path";
|
|
3
3
|
import type { ImageContent, Message, MessageAttribution, ServiceTier, TextContent, Usage } from "@oh-my-pi/pi-ai";
|
|
4
|
-
import {
|
|
4
|
+
import {
|
|
5
|
+
directoryExists,
|
|
6
|
+
getBlobsDir,
|
|
7
|
+
getProjectDir,
|
|
8
|
+
getSessionsDir,
|
|
9
|
+
isEnoent,
|
|
10
|
+
logger,
|
|
11
|
+
toError,
|
|
12
|
+
} from "@oh-my-pi/pi-utils";
|
|
5
13
|
import { ArtifactManager } from "./artifacts";
|
|
6
14
|
import { type BlobPutOptions, type BlobPutResult, BlobStore } from "./blob-store";
|
|
7
15
|
import {
|
|
@@ -743,9 +751,12 @@ export class SessionManager {
|
|
|
743
751
|
|
|
744
752
|
// Adopt the loaded session's working directory. Sessions live in a dir
|
|
745
753
|
// keyed by their cwd, so resuming a session from another project must
|
|
746
|
-
// re-point cwd/sessionDir at that project
|
|
754
|
+
// re-point cwd/sessionDir at that project — unless that project directory
|
|
755
|
+
// no longer exists on disk, in which case adopting it (and the process
|
|
756
|
+
// chdir interactive mode then performs) would fail with ENOENT. Keep the
|
|
757
|
+
// current cwd so the resumed session stays where the user already is.
|
|
747
758
|
const headerCwd = header.cwd ? path.resolve(header.cwd) : undefined;
|
|
748
|
-
if (headerCwd && headerCwd !== path.resolve(this.#cwd)) {
|
|
759
|
+
if (headerCwd && headerCwd !== path.resolve(this.#cwd) && (await directoryExists(headerCwd))) {
|
|
749
760
|
this.#cwd = headerCwd;
|
|
750
761
|
this.#sessionDir = path.dirname(resolvedSessionFile);
|
|
751
762
|
this.#rememberBreadcrumb(this.#cwd, resolvedSessionFile);
|
|
@@ -1562,8 +1573,19 @@ export class SessionManager {
|
|
|
1562
1573
|
): Promise<SessionManager> {
|
|
1563
1574
|
const loaded = await loadEntriesFromFile(filePath, storage);
|
|
1564
1575
|
const header = loaded.find(entry => entry.type === "session") as SessionHeader | undefined;
|
|
1565
|
-
|
|
1566
|
-
|
|
1576
|
+
// Resume into the session's recorded cwd only when that directory still
|
|
1577
|
+
// exists. A deleted project dir would make the constructor's #cwd — and the
|
|
1578
|
+
// `setProjectDir` chdir interactive mode runs next — point at (and fail on)
|
|
1579
|
+
// a missing path, so fall back to the launch cwd and anchor /new and /branch
|
|
1580
|
+
// there too, keeping the resumed session where the user already is.
|
|
1581
|
+
const recordedCwd = header?.cwd;
|
|
1582
|
+
const recordedCwdUsable = !!recordedCwd && (await directoryExists(recordedCwd));
|
|
1583
|
+
const cwd = recordedCwdUsable ? recordedCwd : (options?.initialCwd ?? getProjectDir());
|
|
1584
|
+
const dir =
|
|
1585
|
+
sessionDir ??
|
|
1586
|
+
(recordedCwd && !recordedCwdUsable
|
|
1587
|
+
? SessionManager.getDefaultSessionDir(cwd, undefined, storage)
|
|
1588
|
+
: path.dirname(path.resolve(filePath)));
|
|
1567
1589
|
const manager = new SessionManager(cwd, dir, true, storage);
|
|
1568
1590
|
manager.#suppressBreadcrumb = options?.suppressBreadcrumb === true;
|
|
1569
1591
|
await manager.setSessionFile(filePath);
|
|
@@ -1674,7 +1696,12 @@ export class SessionManager {
|
|
|
1674
1696
|
(newestInTargetDir === null || (newestIsBreadcrumb && !currentProjectAlreadyHasSession));
|
|
1675
1697
|
if (looksLikeMovedProject) {
|
|
1676
1698
|
logger.info("Re-rooting moved session", { from: breadcrumbCwd, to: resolvedCwd });
|
|
1677
|
-
|
|
1699
|
+
// Anchor at the gone breadcrumb cwd so the moveTo below relocates the
|
|
1700
|
+
// session: open() now falls back to the launch cwd for a missing
|
|
1701
|
+
// recorded cwd, which would no-op moveTo when it equals `cwd`.
|
|
1702
|
+
const manager = await SessionManager.open(breadcrumb.sessionFile, undefined, storage, {
|
|
1703
|
+
initialCwd: breadcrumbCwd,
|
|
1704
|
+
});
|
|
1678
1705
|
await manager.moveTo(cwd, sessionDir);
|
|
1679
1706
|
return manager;
|
|
1680
1707
|
}
|
package/src/system-prompt.ts
CHANGED
|
@@ -367,13 +367,17 @@ export function buildSystemPromptToolMetadata(
|
|
|
367
367
|
export interface BuildSystemPromptOptions {
|
|
368
368
|
/** Custom system prompt (replaces default). */
|
|
369
369
|
customPrompt?: string;
|
|
370
|
+
/** Already-loaded custom system prompt text; bypasses path resolution. */
|
|
371
|
+
resolvedCustomPrompt?: string;
|
|
370
372
|
/** Tools to include in prompt. */
|
|
371
373
|
tools?: Map<string, SystemPromptToolMetadata>;
|
|
372
374
|
/** Tool names to include in prompt. */
|
|
373
375
|
toolNames?: string[];
|
|
374
376
|
/** Text to append to system prompt. */
|
|
375
377
|
appendSystemPrompt?: string;
|
|
376
|
-
/**
|
|
378
|
+
/** Already-loaded append prompt text; bypasses path resolution. */
|
|
379
|
+
resolvedAppendSystemPrompt?: string;
|
|
380
|
+
/** Inline full tool descriptors in the system prompt. Default: false */
|
|
377
381
|
inlineToolDescriptors?: boolean;
|
|
378
382
|
/**
|
|
379
383
|
* Whether provider-native tool calling is active (no owned/in-band syntax).
|
|
@@ -431,9 +435,11 @@ export async function buildSystemPrompt(options: BuildSystemPromptOptions = {}):
|
|
|
431
435
|
|
|
432
436
|
const {
|
|
433
437
|
customPrompt,
|
|
438
|
+
resolvedCustomPrompt: providedResolvedCustomPrompt,
|
|
434
439
|
tools,
|
|
435
440
|
appendSystemPrompt,
|
|
436
441
|
inlineToolDescriptors: providedInlineToolDescriptors,
|
|
442
|
+
resolvedAppendSystemPrompt: providedResolvedAppendPrompt,
|
|
437
443
|
nativeTools = true,
|
|
438
444
|
skillsSettings,
|
|
439
445
|
toolNames: providedToolNames,
|
|
@@ -454,7 +460,7 @@ export async function buildSystemPrompt(options: BuildSystemPromptOptions = {}):
|
|
|
454
460
|
model,
|
|
455
461
|
personality = "default",
|
|
456
462
|
} = options;
|
|
457
|
-
const inlineToolDescriptors = providedInlineToolDescriptors ??
|
|
463
|
+
const inlineToolDescriptors = providedInlineToolDescriptors ?? false;
|
|
458
464
|
const resolvedCwd = cwd ?? getProjectDir();
|
|
459
465
|
|
|
460
466
|
const prepDefaults = {
|
|
@@ -500,9 +506,15 @@ export async function buildSystemPrompt(options: BuildSystemPromptOptions = {}):
|
|
|
500
506
|
return result.value;
|
|
501
507
|
}
|
|
502
508
|
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
509
|
+
// Caller-supplied `customPrompt` / `resolvedCustomPrompt` owns block 0; the
|
|
510
|
+
// secondary capability-path `SYSTEM.md` walk-up MUST NOT silently augment it,
|
|
511
|
+
// because that would defeat CLI precedence over project/user `SYSTEM.md`.
|
|
512
|
+
const callerControlsCustomPrompt =
|
|
513
|
+
(typeof providedResolvedCustomPrompt === "string" && providedResolvedCustomPrompt.length > 0) ||
|
|
514
|
+
(typeof customPrompt === "string" && customPrompt.length > 0);
|
|
515
|
+
const systemPromptCustomizationPromise: Promise<string | null> = callerControlsCustomPrompt
|
|
516
|
+
? Promise.resolve(null)
|
|
517
|
+
: logger.time("loadSystemPromptFiles", loadSystemPromptFiles, { cwd: resolvedCwd });
|
|
506
518
|
const contextFilesPromise = providedContextFiles
|
|
507
519
|
? Promise.resolve(providedContextFiles)
|
|
508
520
|
: logger.time("loadProjectContextFiles", loadProjectContextFiles, { cwd: resolvedCwd });
|
|
@@ -523,12 +535,16 @@ export async function buildSystemPrompt(options: BuildSystemPromptOptions = {}):
|
|
|
523
535
|
await Promise.all([
|
|
524
536
|
withDeadline(
|
|
525
537
|
"customPrompt",
|
|
526
|
-
|
|
538
|
+
providedResolvedCustomPrompt !== undefined
|
|
539
|
+
? Promise.resolve(providedResolvedCustomPrompt)
|
|
540
|
+
: resolvePromptInput(customPrompt, "system prompt"),
|
|
527
541
|
prepDefaults.resolvedCustomPrompt,
|
|
528
542
|
),
|
|
529
543
|
withDeadline(
|
|
530
544
|
"appendSystemPrompt",
|
|
531
|
-
|
|
545
|
+
providedResolvedAppendPrompt !== undefined
|
|
546
|
+
? Promise.resolve(providedResolvedAppendPrompt)
|
|
547
|
+
: resolvePromptInput(appendSystemPrompt, "append system prompt"),
|
|
532
548
|
prepDefaults.resolvedAppendPrompt,
|
|
533
549
|
),
|
|
534
550
|
withDeadline(
|
|
@@ -662,7 +678,11 @@ export async function buildSystemPrompt(options: BuildSystemPromptOptions = {}):
|
|
|
662
678
|
};
|
|
663
679
|
const rendered = prompt.render(resolvedCustomPrompt ? customSystemPromptTemplate : systemPromptTemplate, data);
|
|
664
680
|
const systemPrompt = [rendered];
|
|
665
|
-
|
|
681
|
+
// Custom prompt templates already render context files and append text; the
|
|
682
|
+
// project footer still carries environment, cwd, workspace, and dir-context.
|
|
683
|
+
const projectPrompt = prompt
|
|
684
|
+
.render(projectPromptTemplate, resolvedCustomPrompt ? { ...data, contextFiles: [], appendPrompt: "" } : data)
|
|
685
|
+
.trim();
|
|
666
686
|
if (projectPrompt) {
|
|
667
687
|
systemPrompt.push(projectPrompt);
|
|
668
688
|
}
|
package/src/task/executor.ts
CHANGED
|
@@ -302,6 +302,16 @@ export interface ExecutorOptions {
|
|
|
302
302
|
enableLsp?: boolean;
|
|
303
303
|
signal?: AbortSignal;
|
|
304
304
|
onProgress?: (progress: AgentProgress) => void;
|
|
305
|
+
/**
|
|
306
|
+
* Epochs (ms, `Date.now()`) bracketing the concurrency-semaphore wait:
|
|
307
|
+
* `invokedAt` is stamped at the spawn boundary before `acquire()`,
|
|
308
|
+
* `acquiredAt` immediately after. {@link runSubprocess} reports true queue
|
|
309
|
+
* wait (`acquiredAt - invokedAt`) and pre-run setup (`startTime - acquiredAt`)
|
|
310
|
+
* separately in the launch-timing debug log. Undefined for callers that
|
|
311
|
+
* bypass the semaphore path.
|
|
312
|
+
*/
|
|
313
|
+
invokedAt?: number;
|
|
314
|
+
acquiredAt?: number;
|
|
305
315
|
sessionFile?: string | null;
|
|
306
316
|
persistArtifacts?: boolean;
|
|
307
317
|
artifactsDir?: string;
|
|
@@ -1698,6 +1708,9 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
|
|
|
1698
1708
|
onProgress,
|
|
1699
1709
|
} = options;
|
|
1700
1710
|
const startTime = Date.now();
|
|
1711
|
+
// Set by the session's onFirstChatDispatch hook the first time the agent
|
|
1712
|
+
// loop dispatches a chat request to the provider — the launch-complete boundary.
|
|
1713
|
+
let firstChatDispatchAt: number | undefined;
|
|
1701
1714
|
|
|
1702
1715
|
// Check if already aborted
|
|
1703
1716
|
if (signal?.aborted) {
|
|
@@ -1868,6 +1881,14 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
|
|
|
1868
1881
|
abortSignal.removeEventListener("abort", onAbort);
|
|
1869
1882
|
}
|
|
1870
1883
|
};
|
|
1884
|
+
// Launch-latency phase marks (performance.now()); read by the debug log
|
|
1885
|
+
// emitted before this closure returns. Left undefined when setup throws
|
|
1886
|
+
// before reaching the phase, which itself localizes the cost.
|
|
1887
|
+
const perfStart = performance.now();
|
|
1888
|
+
let resolvedAt: number | undefined;
|
|
1889
|
+
let sessionOpenedAt: number | undefined;
|
|
1890
|
+
let sessionCreatedAt: number | undefined;
|
|
1891
|
+
let readyAt: number | undefined;
|
|
1871
1892
|
|
|
1872
1893
|
try {
|
|
1873
1894
|
checkAbort();
|
|
@@ -1935,6 +1956,7 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
|
|
|
1935
1956
|
const effectiveThinkingLevel = explicitThinkingLevel
|
|
1936
1957
|
? resolvedThinkingLevel
|
|
1937
1958
|
: (thinkingLevel ?? resolvedThinkingLevel);
|
|
1959
|
+
resolvedAt = performance.now();
|
|
1938
1960
|
|
|
1939
1961
|
const effectiveCwd = worktree ?? cwd;
|
|
1940
1962
|
const sessionManager = sessionFile
|
|
@@ -1948,6 +1970,7 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
|
|
|
1948
1970
|
if (options.parentArtifactManager) {
|
|
1949
1971
|
sessionManager.adoptArtifactManager(options.parentArtifactManager);
|
|
1950
1972
|
}
|
|
1973
|
+
sessionOpenedAt = performance.now();
|
|
1951
1974
|
|
|
1952
1975
|
const mcpProxyTools = options.mcpManager ? createMCPProxyTools(options.mcpManager) : [];
|
|
1953
1976
|
const enableMCP = !options.mcpManager;
|
|
@@ -2043,6 +2066,9 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
|
|
|
2043
2066
|
localProtocolOptions: options.localProtocolOptions,
|
|
2044
2067
|
telemetry: subagentTelemetry,
|
|
2045
2068
|
parentEvalSessionId: options.parentEvalSessionId,
|
|
2069
|
+
onFirstChatDispatch: () => {
|
|
2070
|
+
firstChatDispatchAt ??= performance.now();
|
|
2071
|
+
},
|
|
2046
2072
|
});
|
|
2047
2073
|
|
|
2048
2074
|
const sessionPromise = createAgentSession(buildSubagentSessionOptions(sessionManager));
|
|
@@ -2056,6 +2082,7 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
|
|
|
2056
2082
|
void sessionPromise.then(created => created.session.dispose()).catch(() => {});
|
|
2057
2083
|
throw err;
|
|
2058
2084
|
}
|
|
2085
|
+
sessionCreatedAt = performance.now();
|
|
2059
2086
|
|
|
2060
2087
|
monitor.setActiveSession(session);
|
|
2061
2088
|
installRegistryStatusSync(session);
|
|
@@ -2201,6 +2228,7 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
|
|
|
2201
2228
|
}
|
|
2202
2229
|
}
|
|
2203
2230
|
|
|
2231
|
+
readyAt = performance.now();
|
|
2204
2232
|
const outcome = await driveSessionToYield(session, monitor, task);
|
|
2205
2233
|
exitCode = outcome.exitCode;
|
|
2206
2234
|
error = outcome.error;
|
|
@@ -2265,6 +2293,35 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
|
|
|
2265
2293
|
}
|
|
2266
2294
|
}
|
|
2267
2295
|
|
|
2296
|
+
// Launch-latency breakdown (subagent invocation → first chat dispatch).
|
|
2297
|
+
// Phase deltas are performance.now() spans; the semaphore brackets use the
|
|
2298
|
+
// Date.now epochs captured by the spawn site (invokedAt before acquire,
|
|
2299
|
+
// acquiredAt after) so queue wait and pre-run setup are reported apart.
|
|
2300
|
+
const span = (from: number | undefined, to: number | undefined): number | undefined =>
|
|
2301
|
+
from !== undefined && to !== undefined ? Math.round(to - from) : undefined;
|
|
2302
|
+
const queueMs =
|
|
2303
|
+
options.invokedAt !== undefined && options.acquiredAt !== undefined
|
|
2304
|
+
? Math.round(options.acquiredAt - options.invokedAt)
|
|
2305
|
+
: undefined;
|
|
2306
|
+
const preRunMs = options.acquiredAt !== undefined ? Math.round(startTime - options.acquiredAt) : undefined;
|
|
2307
|
+
const setupToFirstChatMs = span(perfStart, firstChatDispatchAt);
|
|
2308
|
+
const invokeToFirstChatMs =
|
|
2309
|
+
options.invokedAt !== undefined && setupToFirstChatMs !== undefined
|
|
2310
|
+
? Math.round(startTime - options.invokedAt) + setupToFirstChatMs
|
|
2311
|
+
: undefined;
|
|
2312
|
+
logger.debug("subagent launch timing", {
|
|
2313
|
+
id,
|
|
2314
|
+
agent: agent.name,
|
|
2315
|
+
queueMs,
|
|
2316
|
+
preRunMs,
|
|
2317
|
+
resolveMs: span(perfStart, resolvedAt),
|
|
2318
|
+
sessionOpenMs: span(resolvedAt, sessionOpenedAt),
|
|
2319
|
+
createSessionMs: span(sessionOpenedAt, sessionCreatedAt),
|
|
2320
|
+
readyMs: span(sessionCreatedAt, readyAt),
|
|
2321
|
+
promptToFirstChatMs: span(readyAt, firstChatDispatchAt),
|
|
2322
|
+
setupToFirstChatMs,
|
|
2323
|
+
invokeToFirstChatMs,
|
|
2324
|
+
});
|
|
2268
2325
|
return {
|
|
2269
2326
|
exitCode,
|
|
2270
2327
|
error,
|
package/src/task/index.ts
CHANGED
|
@@ -798,6 +798,7 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
|
|
|
798
798
|
const startedAt = Date.now();
|
|
799
799
|
const semaphore = this.#getSpawnSemaphore();
|
|
800
800
|
await semaphore.acquire();
|
|
801
|
+
const acquiredAt = Date.now();
|
|
801
802
|
if (runSignal.aborted) {
|
|
802
803
|
semaphore.release();
|
|
803
804
|
progress.status = "aborted";
|
|
@@ -819,6 +820,7 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
|
|
|
819
820
|
agentId,
|
|
820
821
|
progress.index,
|
|
821
822
|
true,
|
|
823
|
+
{ invokedAt: startedAt, acquiredAt },
|
|
822
824
|
);
|
|
823
825
|
const finalText = result.content.find(part => part.type === "text")?.text ?? "(no output)";
|
|
824
826
|
const singleResult = result.details?.results[0];
|
|
@@ -900,7 +902,9 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
|
|
|
900
902
|
): Promise<AgentToolResult<TaskToolDetails>> {
|
|
901
903
|
const semaphore = this.#getSpawnSemaphore();
|
|
902
904
|
if (spawnItems.length === 1) {
|
|
905
|
+
const invokedAt = Date.now();
|
|
903
906
|
await semaphore.acquire();
|
|
907
|
+
const acquiredAt = Date.now();
|
|
904
908
|
try {
|
|
905
909
|
return await this.#executeSync(
|
|
906
910
|
toolCallId,
|
|
@@ -909,6 +913,8 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
|
|
|
909
913
|
onUpdate,
|
|
910
914
|
undefined,
|
|
911
915
|
0,
|
|
916
|
+
false,
|
|
917
|
+
{ invokedAt, acquiredAt },
|
|
912
918
|
);
|
|
913
919
|
} finally {
|
|
914
920
|
semaphore.release();
|
|
@@ -935,7 +941,9 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
|
|
|
935
941
|
spawnItems,
|
|
936
942
|
spawnItems.length,
|
|
937
943
|
async (item, index, workerSignal) => {
|
|
944
|
+
const invokedAt = Date.now();
|
|
938
945
|
await semaphore.acquire();
|
|
946
|
+
const acquiredAt = Date.now();
|
|
939
947
|
try {
|
|
940
948
|
const itemOnUpdate: AgentToolUpdateCallback<TaskToolDetails> | undefined = onUpdate
|
|
941
949
|
? update => {
|
|
@@ -953,6 +961,8 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
|
|
|
953
961
|
itemOnUpdate,
|
|
954
962
|
undefined,
|
|
955
963
|
index,
|
|
964
|
+
false,
|
|
965
|
+
{ invokedAt, acquiredAt },
|
|
956
966
|
);
|
|
957
967
|
} finally {
|
|
958
968
|
semaphore.release();
|
|
@@ -1012,8 +1022,9 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
|
|
|
1012
1022
|
preAllocatedId?: string,
|
|
1013
1023
|
spawnIndex = 0,
|
|
1014
1024
|
detached = false,
|
|
1025
|
+
launchTiming?: { invokedAt: number; acquiredAt: number },
|
|
1015
1026
|
): Promise<AgentToolResult<TaskToolDetails>> {
|
|
1016
|
-
return this.#runSpawn(toolCallId, params, signal, onUpdate, preAllocatedId, spawnIndex, detached);
|
|
1027
|
+
return this.#runSpawn(toolCallId, params, signal, onUpdate, preAllocatedId, spawnIndex, detached, launchTiming);
|
|
1017
1028
|
}
|
|
1018
1029
|
|
|
1019
1030
|
/** Spawn a fresh subagent and run it to completion. */
|
|
@@ -1025,6 +1036,7 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
|
|
|
1025
1036
|
preAllocatedId?: string,
|
|
1026
1037
|
spawnIndex = 0,
|
|
1027
1038
|
detached = false,
|
|
1039
|
+
launchTiming?: { invokedAt: number; acquiredAt: number },
|
|
1028
1040
|
): Promise<AgentToolResult<TaskToolDetails>> {
|
|
1029
1041
|
const startTime = Date.now();
|
|
1030
1042
|
const { agents, projectAgentsDir } = await discoverAgents(this.session.cwd);
|
|
@@ -1265,6 +1277,8 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
|
|
|
1265
1277
|
detached,
|
|
1266
1278
|
id: agentId,
|
|
1267
1279
|
taskDepth,
|
|
1280
|
+
invokedAt: launchTiming?.invokedAt,
|
|
1281
|
+
acquiredAt: launchTiming?.acquiredAt,
|
|
1268
1282
|
modelOverride,
|
|
1269
1283
|
parentActiveModelPattern,
|
|
1270
1284
|
thinkingLevel: thinkingLevelOverride,
|
package/src/tools/browser.ts
CHANGED
|
@@ -45,7 +45,7 @@ const browserSchema = type({
|
|
|
45
45
|
),
|
|
46
46
|
"dialogs?": type("'accept' | 'dismiss'").describe("auto-handle dialogs"),
|
|
47
47
|
"code?": type("string").describe("js body to run in tab"),
|
|
48
|
-
"timeout?": type("number").describe("timeout in seconds
|
|
48
|
+
"timeout?": type("number").describe("timeout in seconds"),
|
|
49
49
|
"all?": type("boolean").describe("close every tab"),
|
|
50
50
|
"kill?": type("boolean").describe("also kill spawned-app browsers"),
|
|
51
51
|
});
|
package/src/tools/eval.ts
CHANGED
|
@@ -31,7 +31,7 @@ const evalCellSchema = type({
|
|
|
31
31
|
language: type("'py' | 'js'").describe('runtime: "py" for the IPython kernel, "js" for the persistent JS VM'),
|
|
32
32
|
code: type("string").describe("cell body, verbatim. Use top-level await freely."),
|
|
33
33
|
"title?": type("string").describe('short label shown in transcript (e.g. "imports", "load config")'),
|
|
34
|
-
"timeout?": type("number").describe("per-cell timeout in seconds
|
|
34
|
+
"timeout?": type("number").describe("per-cell timeout in seconds"),
|
|
35
35
|
"reset?": type("boolean").describe(
|
|
36
36
|
"wipe this cell's language kernel before running. Other languages are untouched.",
|
|
37
37
|
),
|
package/src/tools/find.ts
CHANGED
|
@@ -44,13 +44,7 @@ const findSchema = type({
|
|
|
44
44
|
.describe("globs including search paths"),
|
|
45
45
|
"hidden?": type("boolean").describe("include hidden files"),
|
|
46
46
|
"gitignore?": type("boolean").describe("respect gitignore"),
|
|
47
|
-
"limit?": type("number").describe("max results
|
|
48
|
-
"timeout?": type("number").describe("timeout in seconds (0.5–60)"),
|
|
49
|
-
}).narrow((o, ctx) => {
|
|
50
|
-
if (o.timeout !== undefined && (o.timeout < 0.5 || o.timeout > 60)) {
|
|
51
|
-
return ctx.mustBe("a timeout between 0.5 and 60 seconds");
|
|
52
|
-
}
|
|
53
|
-
return true;
|
|
47
|
+
"limit?": type("number").describe("max results"),
|
|
54
48
|
});
|
|
55
49
|
|
|
56
50
|
export type FindToolInput = typeof findSchema.infer;
|
|
@@ -58,8 +52,6 @@ export type FindToolInput = typeof findSchema.infer;
|
|
|
58
52
|
const DEFAULT_LIMIT = 200;
|
|
59
53
|
const MAX_LIMIT = 200;
|
|
60
54
|
const DEFAULT_GLOB_TIMEOUT_MS = 5000;
|
|
61
|
-
const MIN_GLOB_TIMEOUT_MS = 500;
|
|
62
|
-
const MAX_GLOB_TIMEOUT_MS = 60_000;
|
|
63
55
|
|
|
64
56
|
export interface FindToolDetails {
|
|
65
57
|
truncation?: TruncationResult;
|
|
@@ -132,10 +124,6 @@ export class FindTool implements AgentTool<typeof findSchema, FindToolDetails> {
|
|
|
132
124
|
caption: "Find directories matching a name (returns both files and dirs; directories are suffixed with `/`)",
|
|
133
125
|
call: { paths: ["**/tests"] },
|
|
134
126
|
},
|
|
135
|
-
{
|
|
136
|
-
caption: "Long-running search on a slow volume",
|
|
137
|
-
call: { paths: ["/Volumes/Storage/**/*.py"], timeout: 30 },
|
|
138
|
-
},
|
|
139
127
|
];
|
|
140
128
|
readonly strict = true;
|
|
141
129
|
|
|
@@ -156,7 +144,7 @@ export class FindTool implements AgentTool<typeof findSchema, FindToolDetails> {
|
|
|
156
144
|
onUpdate?: AgentToolUpdateCallback<FindToolDetails>,
|
|
157
145
|
_context?: AgentToolContext,
|
|
158
146
|
): Promise<AgentToolResult<FindToolDetails>> {
|
|
159
|
-
const { paths, limit, hidden, gitignore
|
|
147
|
+
const { paths, limit, hidden, gitignore } = params;
|
|
160
148
|
|
|
161
149
|
return untilAborted(signal, async () => {
|
|
162
150
|
const formatScopePath = (targetPath: string): string => formatPathRelativeToCwd(targetPath, this.session.cwd);
|
|
@@ -237,8 +225,7 @@ export class FindTool implements AgentTool<typeof findSchema, FindToolDetails> {
|
|
|
237
225
|
const effectiveLimit = Math.min(MAX_LIMIT, Math.max(1, Math.floor(requestedLimit)));
|
|
238
226
|
const includeHidden = hidden ?? true;
|
|
239
227
|
const useGitignore = gitignore ?? true;
|
|
240
|
-
const
|
|
241
|
-
const timeoutMs = Math.min(MAX_GLOB_TIMEOUT_MS, Math.max(MIN_GLOB_TIMEOUT_MS, requestedTimeoutMs));
|
|
228
|
+
const timeoutMs = DEFAULT_GLOB_TIMEOUT_MS;
|
|
242
229
|
const timeoutSignal = AbortSignal.timeout(timeoutMs);
|
|
243
230
|
const combinedSignal = signal ? AbortSignal.any([signal, timeoutSignal]) : timeoutSignal;
|
|
244
231
|
const formatMatchPath = (matchPath: string, base: string, fileType?: natives.FileType): string => {
|
|
@@ -448,7 +435,7 @@ export class FindTool implements AgentTool<typeof findSchema, FindToolDetails> {
|
|
|
448
435
|
partial.sort((a, b) => b.m - a.m);
|
|
449
436
|
const sortedPaths = partial.map(entry => entry.p);
|
|
450
437
|
const seconds = timeoutMs % 1000 === 0 ? `${timeoutMs / 1000}` : (timeoutMs / 1000).toFixed(1);
|
|
451
|
-
const notice = `find timed out after ${seconds}s; returning ${sortedPaths.length} partial matches —
|
|
438
|
+
const notice = `find timed out after ${seconds}s; returning ${sortedPaths.length} partial matches — narrow the pattern instead of retrying blindly`;
|
|
452
439
|
return buildResult(sortedPaths, { notice, forceTruncated: true });
|
|
453
440
|
}
|
|
454
441
|
|
package/src/tools/memory-edit.ts
CHANGED
|
@@ -7,7 +7,7 @@ const memoryEditSchema = type({
|
|
|
7
7
|
op: type("'update' | 'forget' | 'invalidate'").describe("memory edit operation"),
|
|
8
8
|
id: type("string").describe("memory id from recall output"),
|
|
9
9
|
"content?": type("string").describe("replacement content for update"),
|
|
10
|
-
"importance?": type("number").describe("replacement importance for update
|
|
10
|
+
"importance?": type("number").describe("replacement importance for update (0–1)"),
|
|
11
11
|
"replacement_id?": type("string").describe("replacement memory id for invalidate"),
|
|
12
12
|
});
|
|
13
13
|
|
package/src/tools/search.ts
CHANGED
|
@@ -70,7 +70,7 @@ const searchSchema = type({
|
|
|
70
70
|
.describe(
|
|
71
71
|
'file, directory, glob, internal URL, or array of those to search; append `:<lines>` to scope a file to specific line ranges. Omitted or empty -> searches the workspace root (".")',
|
|
72
72
|
),
|
|
73
|
-
"
|
|
73
|
+
"case?": type("boolean").describe("case-sensitive search"),
|
|
74
74
|
"gitignore?": type("boolean").describe("respect gitignore"),
|
|
75
75
|
"skip?": type("number")
|
|
76
76
|
.or("null")
|
|
@@ -680,7 +680,7 @@ export class SearchTool implements AgentTool<typeof searchSchema, SearchToolDeta
|
|
|
680
680
|
_onUpdate?: AgentToolUpdateCallback<SearchToolDetails>,
|
|
681
681
|
_toolContext?: AgentToolContext,
|
|
682
682
|
): Promise<AgentToolResult<SearchToolDetails>> {
|
|
683
|
-
const { pattern, paths: rawPaths,
|
|
683
|
+
const { pattern, paths: rawPaths, case: caseSensitive, gitignore, skip } = params;
|
|
684
684
|
|
|
685
685
|
return untilAborted(signal, async () => {
|
|
686
686
|
// Preserve the pattern verbatim — leading/trailing whitespace is
|
|
@@ -763,7 +763,7 @@ export class SearchTool implements AgentTool<typeof searchSchema, SearchToolDeta
|
|
|
763
763
|
}
|
|
764
764
|
const normalizedContextBefore = this.session.settings.get("search.contextBefore");
|
|
765
765
|
const normalizedContextAfter = this.session.settings.get("search.contextAfter");
|
|
766
|
-
const ignoreCase =
|
|
766
|
+
const ignoreCase = !(caseSensitive ?? true);
|
|
767
767
|
const useGitignore = gitignore ?? true;
|
|
768
768
|
const patternHasNewline = normalizedPattern.includes("\n") || normalizedPattern.includes("\\n");
|
|
769
769
|
const effectiveMultiline = patternHasNewline;
|
|
@@ -1272,7 +1272,7 @@ export class SearchTool implements AgentTool<typeof searchSchema, SearchToolDeta
|
|
|
1272
1272
|
interface SearchRenderArgs {
|
|
1273
1273
|
pattern: string;
|
|
1274
1274
|
paths?: string | string[];
|
|
1275
|
-
|
|
1275
|
+
case?: boolean;
|
|
1276
1276
|
gitignore?: boolean;
|
|
1277
1277
|
skip?: number;
|
|
1278
1278
|
}
|
|
@@ -1443,7 +1443,7 @@ export const searchToolRenderer = {
|
|
|
1443
1443
|
const paths = toPathList(args.paths);
|
|
1444
1444
|
const meta: string[] = [];
|
|
1445
1445
|
if (paths.length) meta.push(`in ${paths.join(", ")}`);
|
|
1446
|
-
if (args.
|
|
1446
|
+
if (args.case === false) meta.push("case:insensitive");
|
|
1447
1447
|
if (args.gitignore === false) meta.push("gitignore:false");
|
|
1448
1448
|
if (args.skip !== undefined && args.skip > 0) meta.push(`skip:${args.skip}`);
|
|
1449
1449
|
|