@adibacsi/pi-jack 1.0.3 → 1.0.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/CHANGELOG.md
CHANGED
|
@@ -8,6 +8,13 @@ All notable changes to this project are documented here. The format follows
|
|
|
8
8
|
|
|
9
9
|
Nothing yet.
|
|
10
10
|
|
|
11
|
+
## [1.0.4] - 2026-10-04
|
|
12
|
+
|
|
13
|
+
### Changed
|
|
14
|
+
|
|
15
|
+
- The extension's source files moved into `src/`, and the package's `main`, `exports` and `pi.extensions` point at
|
|
16
|
+
`src/index.ts`. Pi picks up the new location by itself; nothing changes for an installed package.
|
|
17
|
+
|
|
11
18
|
## [1.0.3] - 2026-10-04
|
|
12
19
|
|
|
13
20
|
### Changed
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@adibacsi/pi-jack",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.4",
|
|
4
4
|
"description": "JACK (JSON Agent Contractor Kit): Pi extension for delegating to subagents under a JSON Schema contract, for structured, reliable, and type-safe agent responses.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"pi",
|
|
@@ -18,16 +18,13 @@
|
|
|
18
18
|
"license": "MIT",
|
|
19
19
|
"author": "Adam Jakab",
|
|
20
20
|
"type": "module",
|
|
21
|
-
"main": "index.ts",
|
|
22
|
-
"exports": "./index.ts",
|
|
21
|
+
"main": "src/index.ts",
|
|
22
|
+
"exports": "./src/index.ts",
|
|
23
23
|
"engines": {
|
|
24
24
|
"node": ">=22.19.0"
|
|
25
25
|
},
|
|
26
26
|
"files": [
|
|
27
|
-
"
|
|
28
|
-
"agents.ts",
|
|
29
|
-
"contract.ts",
|
|
30
|
-
"json-schema.ts",
|
|
27
|
+
"src/",
|
|
31
28
|
"agents/",
|
|
32
29
|
"prompts/",
|
|
33
30
|
"CHANGELOG.md"
|
|
@@ -38,7 +35,7 @@
|
|
|
38
35
|
"pi": {
|
|
39
36
|
"image": "https://raw.githubusercontent.com/adamjakab/pi-jack/main/docs/logo.png",
|
|
40
37
|
"extensions": [
|
|
41
|
-
"./index.ts"
|
|
38
|
+
"./src/index.ts"
|
|
42
39
|
],
|
|
43
40
|
"prompts": [
|
|
44
41
|
"./prompts/*.md"
|
|
@@ -47,6 +44,7 @@
|
|
|
47
44
|
"scripts": {
|
|
48
45
|
"test": "vitest run",
|
|
49
46
|
"test:watch": "vitest",
|
|
47
|
+
"coverage": "vitest run --coverage",
|
|
50
48
|
"test:e2e": "node tests/e2e/run.ts",
|
|
51
49
|
"typecheck": "node scripts/pi-modules.mjs && tsc -p tsconfig.json",
|
|
52
50
|
"format": "prettier --write .",
|
|
@@ -70,6 +68,7 @@
|
|
|
70
68
|
},
|
|
71
69
|
"devDependencies": {
|
|
72
70
|
"@types/node": "^26.6.4",
|
|
71
|
+
"@vitest/coverage-v8": "^5.0.3",
|
|
73
72
|
"prettier": "^3.9.9",
|
|
74
73
|
"typescript": "^7.0.2",
|
|
75
74
|
"vitest": "^5.0.3"
|
|
@@ -15,7 +15,7 @@ import {
|
|
|
15
15
|
|
|
16
16
|
/** Folder of the agents that ship with this extension. */
|
|
17
17
|
export const BUILT_IN_AGENTS_DIR = fileURLToPath(
|
|
18
|
-
new URL("
|
|
18
|
+
new URL("../agents", import.meta.url),
|
|
19
19
|
);
|
|
20
20
|
|
|
21
21
|
/** Where an agent comes from: this extension, or the user's agents folder. */
|
|
@@ -18,22 +18,11 @@
|
|
|
18
18
|
|
|
19
19
|
import { spawn } from "node:child_process";
|
|
20
20
|
import * as fs from "node:fs";
|
|
21
|
-
import * as os from "node:os";
|
|
22
|
-
import * as path from "node:path";
|
|
23
21
|
import type { Message } from "@earendil-works/pi-ai";
|
|
24
|
-
import {
|
|
25
|
-
defineTool,
|
|
26
|
-
type ExtensionAPI,
|
|
27
|
-
withFileMutationQueue,
|
|
28
|
-
} from "@earendil-works/pi-coding-agent";
|
|
22
|
+
import { defineTool, type ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
29
23
|
import { fileURLToPath } from "node:url";
|
|
30
24
|
import { Type, type TSchema } from "typebox";
|
|
31
|
-
import {
|
|
32
|
-
type AgentConfig,
|
|
33
|
-
type AgentLoadError,
|
|
34
|
-
type AgentSource,
|
|
35
|
-
discoverAgents,
|
|
36
|
-
} from "./agents.ts";
|
|
25
|
+
import { type AgentSource, discoverAgents } from "./agents.ts";
|
|
37
26
|
import { registerJsonSchema } from "./json-schema.ts";
|
|
38
27
|
import {
|
|
39
28
|
DEFAULT_SCHEMA,
|
|
@@ -46,6 +35,17 @@ import {
|
|
|
46
35
|
type ThinkingLevel,
|
|
47
36
|
schemaErrors,
|
|
48
37
|
} from "./contract.ts";
|
|
38
|
+
import {
|
|
39
|
+
describeAgent,
|
|
40
|
+
describeLoadErrors,
|
|
41
|
+
getFinalAssistantText,
|
|
42
|
+
getPiInvocation,
|
|
43
|
+
mapWithLimit,
|
|
44
|
+
normalizeTools,
|
|
45
|
+
shellQuote,
|
|
46
|
+
taskLabel,
|
|
47
|
+
writeTempFiles,
|
|
48
|
+
} from "./utils.ts";
|
|
49
49
|
|
|
50
50
|
export const MAX_PARALLEL = 2;
|
|
51
51
|
|
|
@@ -58,59 +58,6 @@ const THIS_EXTENSION = fileURLToPath(import.meta.url);
|
|
|
58
58
|
/** Agent used when a call omits `agent`. */
|
|
59
59
|
export const DEFAULT_AGENT = "worker";
|
|
60
60
|
|
|
61
|
-
function getPiInvocation(args: string[]): { command: string; args: string[] } {
|
|
62
|
-
const currentScript = process.argv[1];
|
|
63
|
-
const isBunVirtualScript = currentScript?.startsWith("/$bunfs/root/");
|
|
64
|
-
if (currentScript && !isBunVirtualScript && fs.existsSync(currentScript)) {
|
|
65
|
-
return { command: process.execPath, args: [currentScript, ...args] };
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
const execName = path.basename(process.execPath).toLowerCase();
|
|
69
|
-
const isGenericRuntime = /^(node|bun)(\.exe)?$/.test(execName);
|
|
70
|
-
if (!isGenericRuntime) {
|
|
71
|
-
return { command: process.execPath, args };
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
return { command: "pi", args };
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
/** Writes the child's schema and, when there is one, its system prompt into a fresh private temp dir. */
|
|
78
|
-
async function writeTempFiles(
|
|
79
|
-
agentName: string,
|
|
80
|
-
schema: TSchema,
|
|
81
|
-
prompt: string,
|
|
82
|
-
): Promise<{ dir: string; schemaPath: string; promptPath?: string }> {
|
|
83
|
-
const dir = await fs.promises.mkdtemp(path.join(os.tmpdir(), "pi-jack-"));
|
|
84
|
-
const write = (filePath: string, text: string) =>
|
|
85
|
-
withFileMutationQueue(filePath, () =>
|
|
86
|
-
fs.promises.writeFile(filePath, text, { encoding: "utf-8", mode: 0o600 }),
|
|
87
|
-
);
|
|
88
|
-
|
|
89
|
-
const schemaPath = path.join(dir, "schema.json");
|
|
90
|
-
await write(schemaPath, JSON.stringify(schema));
|
|
91
|
-
if (!prompt) return { dir, schemaPath };
|
|
92
|
-
|
|
93
|
-
const promptPath = path.join(
|
|
94
|
-
dir,
|
|
95
|
-
`prompt-${agentName.replace(/[^\w.-]+/g, "_")}.md`,
|
|
96
|
-
);
|
|
97
|
-
await write(promptPath, prompt);
|
|
98
|
-
return { dir, schemaPath, promptPath };
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
export function normalizeTools(value: unknown): string[] | undefined {
|
|
102
|
-
const raw = Array.isArray(value)
|
|
103
|
-
? value
|
|
104
|
-
: typeof value === "string"
|
|
105
|
-
? value.split(",")
|
|
106
|
-
: [];
|
|
107
|
-
const tools = raw
|
|
108
|
-
.filter((t): t is string => typeof t === "string")
|
|
109
|
-
.map((t) => t.trim())
|
|
110
|
-
.filter(Boolean);
|
|
111
|
-
return tools.length > 0 ? tools : undefined;
|
|
112
|
-
}
|
|
113
|
-
|
|
114
61
|
interface SubagentUsage {
|
|
115
62
|
turns: number;
|
|
116
63
|
input: number;
|
|
@@ -142,7 +89,7 @@ interface RanWith {
|
|
|
142
89
|
}
|
|
143
90
|
|
|
144
91
|
/** How a subagent was set up once defaults and overrides were applied; shown by `debug_mode`. */
|
|
145
|
-
|
|
92
|
+
interface SubagentSetup {
|
|
146
93
|
agent: string;
|
|
147
94
|
/** Where the agent file came from; undefined for a bare pi agent (no default agent file). */
|
|
148
95
|
agentSource?: AgentSource;
|
|
@@ -163,11 +110,8 @@ export interface SubagentSetup {
|
|
|
163
110
|
command: string[];
|
|
164
111
|
}
|
|
165
112
|
|
|
166
|
-
const shellQuote = (arg: string) =>
|
|
167
|
-
/^[\w@%+=:,./-]+$/.test(arg) ? arg : `'${arg.replace(/'/g, "'\\''")}'`;
|
|
168
|
-
|
|
169
113
|
/** Renders a setup as indented lines, for the progress display and the result. */
|
|
170
|
-
|
|
114
|
+
function describeSetup(setup: SubagentSetup): string {
|
|
171
115
|
const from = (source: string | undefined) =>
|
|
172
116
|
source ? ` (from the ${source})` : "";
|
|
173
117
|
const agent = !setup.agentSource
|
|
@@ -191,6 +135,7 @@ export function describeSetup(setup: SubagentSetup): string {
|
|
|
191
135
|
.join("\n");
|
|
192
136
|
}
|
|
193
137
|
|
|
138
|
+
/** Builds the optional `thinking` parameter schema: one of THINKING_LEVELS, with the given description. */
|
|
194
139
|
const thinkingSchema = (description: string) =>
|
|
195
140
|
Type.Optional(
|
|
196
141
|
Type.Union(
|
|
@@ -250,6 +195,7 @@ const outputSchema = Type.Object({
|
|
|
250
195
|
),
|
|
251
196
|
});
|
|
252
197
|
|
|
198
|
+
/** Returns a fresh usage record with every counter at zero. */
|
|
253
199
|
const zeroUsage = (): SubagentUsage => ({
|
|
254
200
|
turns: 0,
|
|
255
201
|
input: 0,
|
|
@@ -259,6 +205,11 @@ const zeroUsage = (): SubagentUsage => ({
|
|
|
259
205
|
cost: 0,
|
|
260
206
|
});
|
|
261
207
|
|
|
208
|
+
/**
|
|
209
|
+
* Runs one task in a child pi. Resolves its agent, system prompt, tools, model, thinking level, and schema from the
|
|
210
|
+
* call and the agent file, spawns the child, and turns its outcome into a result. Never throws for a task that fails;
|
|
211
|
+
* the failure is reported in the result's `error`.
|
|
212
|
+
*/
|
|
262
213
|
async function runSingleSubagent(
|
|
263
214
|
taskText: string,
|
|
264
215
|
agentNameInput: string | undefined,
|
|
@@ -283,6 +234,7 @@ async function runSingleSubagent(
|
|
|
283
234
|
let schemaSource: unknown = schemaInput;
|
|
284
235
|
let schemaBaseDir = process.cwd();
|
|
285
236
|
|
|
237
|
+
/** A result for a task that failed before any child was started. */
|
|
286
238
|
const failure = (error: string): SingleResult => ({
|
|
287
239
|
success: false,
|
|
288
240
|
parsed: false,
|
|
@@ -416,6 +368,7 @@ async function runSingleSubagent(
|
|
|
416
368
|
|
|
417
369
|
const usage = zeroUsage();
|
|
418
370
|
const run = await runChild(args, usage, signal, onProgress);
|
|
371
|
+
/** A result for the finished child: successful when there is no `error`. */
|
|
419
372
|
const result = (
|
|
420
373
|
error: string | undefined,
|
|
421
374
|
data: unknown = null,
|
|
@@ -508,6 +461,7 @@ async function runChild(
|
|
|
508
461
|
let rejections = 0;
|
|
509
462
|
let buffer = "";
|
|
510
463
|
|
|
464
|
+
/** Handles one JSON event from the child: counts usage, reports progress, and records answers and rejections. */
|
|
511
465
|
const handleEvent = (event: any) => {
|
|
512
466
|
if (event.type === "message_end" && event.message) {
|
|
513
467
|
const msg = event.message as Message;
|
|
@@ -595,26 +549,6 @@ async function runChild(
|
|
|
595
549
|
return run;
|
|
596
550
|
}
|
|
597
551
|
|
|
598
|
-
export async function mapWithLimit<TIn, TOut>(
|
|
599
|
-
items: TIn[],
|
|
600
|
-
concurrency: number,
|
|
601
|
-
fn: (item: TIn, index: number) => Promise<TOut>,
|
|
602
|
-
): Promise<TOut[]> {
|
|
603
|
-
if (items.length === 0) return [];
|
|
604
|
-
const limit = Math.max(1, Math.min(concurrency, items.length));
|
|
605
|
-
const results: TOut[] = new Array(items.length);
|
|
606
|
-
let nextIndex = 0;
|
|
607
|
-
const workers = Array.from({ length: limit }).map(async () => {
|
|
608
|
-
while (true) {
|
|
609
|
-
const current = nextIndex++;
|
|
610
|
-
if (current >= items.length) return;
|
|
611
|
-
results[current] = await fn(items[current], current);
|
|
612
|
-
}
|
|
613
|
-
});
|
|
614
|
-
await Promise.all(workers);
|
|
615
|
-
return results;
|
|
616
|
-
}
|
|
617
|
-
|
|
618
552
|
const jackTool = defineTool({
|
|
619
553
|
name: "jack",
|
|
620
554
|
label: "JACK",
|
|
@@ -697,6 +631,10 @@ const jackTool = defineTool({
|
|
|
697
631
|
}),
|
|
698
632
|
outputSchema,
|
|
699
633
|
|
|
634
|
+
/**
|
|
635
|
+
* Runs the call's task, or each of its `tasks` with missing fields taken from the top-level params, streaming
|
|
636
|
+
* per-task progress, and returns every subagent's answer to the model.
|
|
637
|
+
*/
|
|
700
638
|
async execute(_toolCallId, params, signal, onUpdate) {
|
|
701
639
|
// Build normalized task list
|
|
702
640
|
const taskItems: Array<{
|
|
@@ -744,14 +682,7 @@ const jackTool = defineTool({
|
|
|
744
682
|
task: "",
|
|
745
683
|
error: "Either `task` or `tasks` must be provided.",
|
|
746
684
|
attempts: 0,
|
|
747
|
-
usage:
|
|
748
|
-
turns: 0,
|
|
749
|
-
input: 0,
|
|
750
|
-
output: 0,
|
|
751
|
-
cacheRead: 0,
|
|
752
|
-
cacheWrite: 0,
|
|
753
|
-
cost: 0,
|
|
754
|
-
},
|
|
685
|
+
usage: zeroUsage(),
|
|
755
686
|
},
|
|
756
687
|
],
|
|
757
688
|
} as any,
|
|
@@ -771,6 +702,7 @@ const jackTool = defineTool({
|
|
|
771
702
|
);
|
|
772
703
|
const debug = params.debug_mode === true;
|
|
773
704
|
const startedAt = Date.now();
|
|
705
|
+
/** Streams how many tasks are done and each task's current status, plus its setup with `debug_mode`. */
|
|
774
706
|
const reportProgress = () => {
|
|
775
707
|
const done = status.filter(
|
|
776
708
|
(s) => s.startsWith("✓") || s.startsWith("✗"),
|
|
@@ -868,41 +800,10 @@ const jackTool = defineTool({
|
|
|
868
800
|
},
|
|
869
801
|
});
|
|
870
802
|
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
export function getFinalAssistantText(messages: Message[]): string | undefined {
|
|
877
|
-
for (let i = messages.length - 1; i >= 0; i--) {
|
|
878
|
-
const msg = messages[i];
|
|
879
|
-
if (msg.role === "assistant") {
|
|
880
|
-
for (const part of msg.content) {
|
|
881
|
-
if (part.type === "text") return part.text;
|
|
882
|
-
}
|
|
883
|
-
}
|
|
884
|
-
}
|
|
885
|
-
return undefined;
|
|
886
|
-
}
|
|
887
|
-
|
|
888
|
-
function describeLoadErrors(errors: AgentLoadError[]): string {
|
|
889
|
-
return errors
|
|
890
|
-
.map(
|
|
891
|
-
(e) =>
|
|
892
|
-
`${e.file}${e.source === "built-in" ? " [built-in]" : ""} (${e.message})`,
|
|
893
|
-
)
|
|
894
|
-
.join("; ");
|
|
895
|
-
}
|
|
896
|
-
|
|
897
|
-
function describeAgent(agent: AgentConfig): string {
|
|
898
|
-
const origin = agent.overridesBuiltIn
|
|
899
|
-
? " (yours, overrides built-in)"
|
|
900
|
-
: agent.source === "built-in"
|
|
901
|
-
? " (built-in)"
|
|
902
|
-
: "";
|
|
903
|
-
return `- ${agent.name}${origin}: ${agent.description}`;
|
|
904
|
-
}
|
|
905
|
-
|
|
803
|
+
/**
|
|
804
|
+
* The extension's entry point: registers the `--json-schema` flag and its result tools, and the `jack` tool with the
|
|
805
|
+
* available agents listed in its description. Warns at session start about agent files that failed to load.
|
|
806
|
+
*/
|
|
906
807
|
export default function (pi: ExtensionAPI) {
|
|
907
808
|
registerJsonSchema(pi);
|
|
908
809
|
// The prompt templates in prompts/ are declared by the `pi.prompts` manifest entry instead of a
|
package/src/utils.ts
ADDED
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Helpers for the `jack` tool in index.ts that don't depend on its state: launching pi, temp files, formatting, and
|
|
3
|
+
* concurrency.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import * as fs from "node:fs";
|
|
7
|
+
import * as os from "node:os";
|
|
8
|
+
import * as path from "node:path";
|
|
9
|
+
import type { Message } from "@earendil-works/pi-ai";
|
|
10
|
+
import { withFileMutationQueue } from "@earendil-works/pi-coding-agent";
|
|
11
|
+
import type { TSchema } from "typebox";
|
|
12
|
+
import type { AgentConfig, AgentLoadError } from "./agents.ts";
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Works out how to start a child pi with `args`: through the same runtime and script as this process when it is a
|
|
16
|
+
* script on disk, through this executable when pi is a compiled binary, else through `pi` on the PATH.
|
|
17
|
+
*/
|
|
18
|
+
export function getPiInvocation(args: string[]): {
|
|
19
|
+
command: string;
|
|
20
|
+
args: string[];
|
|
21
|
+
} {
|
|
22
|
+
const currentScript = process.argv[1];
|
|
23
|
+
const isBunVirtualScript = currentScript?.startsWith("/$bunfs/root/");
|
|
24
|
+
if (currentScript && !isBunVirtualScript && fs.existsSync(currentScript)) {
|
|
25
|
+
return { command: process.execPath, args: [currentScript, ...args] };
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const execName = path.basename(process.execPath).toLowerCase();
|
|
29
|
+
const isGenericRuntime = /^(node|bun)(\.exe)?$/.test(execName);
|
|
30
|
+
if (!isGenericRuntime) {
|
|
31
|
+
return { command: process.execPath, args };
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return { command: "pi", args };
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** Writes the child's schema and, when there is one, its system prompt into a fresh private temp dir. */
|
|
38
|
+
export async function writeTempFiles(
|
|
39
|
+
agentName: string,
|
|
40
|
+
schema: TSchema,
|
|
41
|
+
prompt: string,
|
|
42
|
+
): Promise<{ dir: string; schemaPath: string; promptPath?: string }> {
|
|
43
|
+
const dir = await fs.promises.mkdtemp(path.join(os.tmpdir(), "pi-jack-"));
|
|
44
|
+
const write = (filePath: string, text: string) =>
|
|
45
|
+
withFileMutationQueue(filePath, () =>
|
|
46
|
+
fs.promises.writeFile(filePath, text, { encoding: "utf-8", mode: 0o600 }),
|
|
47
|
+
);
|
|
48
|
+
|
|
49
|
+
const schemaPath = path.join(dir, "schema.json");
|
|
50
|
+
await write(schemaPath, JSON.stringify(schema));
|
|
51
|
+
if (!prompt) return { dir, schemaPath };
|
|
52
|
+
|
|
53
|
+
const promptPath = path.join(
|
|
54
|
+
dir,
|
|
55
|
+
`prompt-${agentName.replace(/[^\w.-]+/g, "_")}.md`,
|
|
56
|
+
);
|
|
57
|
+
await write(promptPath, prompt);
|
|
58
|
+
return { dir, schemaPath, promptPath };
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Turns a `tools` parameter, an array or a comma-separated string, into a list of trimmed tool names. Returns
|
|
63
|
+
* undefined when no name is left, meaning pi's default tools.
|
|
64
|
+
*/
|
|
65
|
+
export function normalizeTools(value: unknown): string[] | undefined {
|
|
66
|
+
const raw = Array.isArray(value)
|
|
67
|
+
? value
|
|
68
|
+
: typeof value === "string"
|
|
69
|
+
? value.split(",")
|
|
70
|
+
: [];
|
|
71
|
+
const tools = raw
|
|
72
|
+
.filter((t): t is string => typeof t === "string")
|
|
73
|
+
.map((t) => t.trim())
|
|
74
|
+
.filter(Boolean);
|
|
75
|
+
return tools.length > 0 ? tools : undefined;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** Quotes a command-line argument for a POSIX shell, leaving it bare when it needs no quoting. */
|
|
79
|
+
export function shellQuote(arg: string): string {
|
|
80
|
+
return /^[\w@%+=:,./-]+$/.test(arg) ? arg : `'${arg.replace(/'/g, "'\\''")}'`;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Runs `fn` over `items` with at most `concurrency` calls in flight, and returns the results in the order of
|
|
85
|
+
* `items`.
|
|
86
|
+
*/
|
|
87
|
+
export async function mapWithLimit<TIn, TOut>(
|
|
88
|
+
items: TIn[],
|
|
89
|
+
concurrency: number,
|
|
90
|
+
fn: (item: TIn, index: number) => Promise<TOut>,
|
|
91
|
+
): Promise<TOut[]> {
|
|
92
|
+
if (items.length === 0) return [];
|
|
93
|
+
const limit = Math.max(1, Math.min(concurrency, items.length));
|
|
94
|
+
const results: TOut[] = new Array(items.length);
|
|
95
|
+
let nextIndex = 0;
|
|
96
|
+
const workers = Array.from({ length: limit }).map(async () => {
|
|
97
|
+
while (true) {
|
|
98
|
+
const current = nextIndex++;
|
|
99
|
+
if (current >= items.length) return;
|
|
100
|
+
results[current] = await fn(items[current], current);
|
|
101
|
+
}
|
|
102
|
+
});
|
|
103
|
+
await Promise.all(workers);
|
|
104
|
+
return results;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/** Shortens a task to its first line, at most 60 characters, with "..." when anything was cut. */
|
|
108
|
+
export function taskLabel(task: string): string {
|
|
109
|
+
const firstLine = task.split("\n")[0];
|
|
110
|
+
return `${firstLine.slice(0, 60)}${firstLine.length > 60 || firstLine !== task ? "..." : ""}`;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/** Returns the first text part of the last assistant message, or undefined when there is none. */
|
|
114
|
+
export function getFinalAssistantText(messages: Message[]): string | undefined {
|
|
115
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
116
|
+
const msg = messages[i];
|
|
117
|
+
if (msg.role === "assistant") {
|
|
118
|
+
for (const part of msg.content) {
|
|
119
|
+
if (part.type === "text") return part.text;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
return undefined;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/** Lists agent files that failed to load, with their errors, on one line. */
|
|
127
|
+
export function describeLoadErrors(errors: AgentLoadError[]): string {
|
|
128
|
+
return errors
|
|
129
|
+
.map(
|
|
130
|
+
(e) =>
|
|
131
|
+
`${e.file}${e.source === "built-in" ? " [built-in]" : ""} (${e.message})`,
|
|
132
|
+
)
|
|
133
|
+
.join("; ");
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/** Renders an agent as a bullet line for the tool description: its name, origin, and description. */
|
|
137
|
+
export function describeAgent(agent: AgentConfig): string {
|
|
138
|
+
const origin = agent.overridesBuiltIn
|
|
139
|
+
? " (yours, overrides built-in)"
|
|
140
|
+
: agent.source === "built-in"
|
|
141
|
+
? " (built-in)"
|
|
142
|
+
: "";
|
|
143
|
+
return `- ${agent.name}${origin}: ${agent.description}`;
|
|
144
|
+
}
|
|
File without changes
|
|
File without changes
|