@oh-my-pi/pi-coding-agent 16.0.0 → 16.0.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 +140 -133
- package/dist/cli.js +250 -218
- package/dist/types/config/model-resolver.d.ts +14 -0
- package/dist/types/config/settings-schema.d.ts +22 -0
- package/dist/types/discovery/helpers.d.ts +7 -0
- package/dist/types/eval/__tests__/prelude-agent.test.d.ts +1 -0
- package/dist/types/exec/non-interactive-env.d.ts +2 -0
- package/dist/types/extensibility/plugins/runtime-config.d.ts +3 -0
- package/dist/types/modes/types.d.ts +5 -0
- package/dist/types/session/agent-session.d.ts +11 -1
- package/dist/types/session/messages.d.ts +3 -0
- package/dist/types/session/session-manager.d.ts +4 -1
- package/dist/types/task/index.d.ts +21 -0
- package/dist/types/tools/github-cache.d.ts +5 -4
- package/dist/types/tools/job.d.ts +1 -0
- package/dist/types/utils/markit.d.ts +8 -0
- package/dist/types/web/search/index.d.ts +2 -2
- package/dist/types/web/search/provider.d.ts +2 -0
- package/package.json +12 -12
- package/src/advisor/__tests__/advisor.test.ts +44 -0
- package/src/cli/args.ts +2 -0
- package/src/collab/host.ts +1 -1
- package/src/config/model-resolver.ts +35 -1
- package/src/config/settings-schema.ts +23 -1
- package/src/discovery/claude-plugins.ts +3 -42
- package/src/discovery/github.ts +189 -6
- package/src/discovery/helpers.ts +11 -0
- package/src/eval/__tests__/prelude-agent.test.ts +73 -0
- package/src/eval/js/shared/prelude.txt +12 -3
- package/src/eval/py/prelude.py +26 -2
- package/src/exec/bash-executor.ts +2 -2
- package/src/exec/non-interactive-env.ts +71 -0
- package/src/extensibility/custom-commands/bundled/review/index.ts +289 -80
- package/src/extensibility/extensions/runner.ts +17 -1
- package/src/extensibility/plugins/loader.ts +157 -23
- package/src/extensibility/plugins/manager.ts +44 -36
- package/src/extensibility/plugins/marketplace/fetcher.ts +32 -34
- package/src/extensibility/plugins/runtime-config.ts +9 -0
- package/src/internal-urls/docs-index.generated.ts +9 -9
- package/src/internal-urls/issue-pr-protocol.ts +8 -4
- package/src/main.ts +5 -1
- package/src/modes/acp/acp-agent.ts +3 -3
- package/src/modes/components/settings-defs.ts +7 -0
- package/src/modes/components/tips.txt +1 -1
- package/src/modes/controllers/extension-ui-controller.ts +4 -3
- package/src/modes/controllers/input-controller.ts +1 -0
- package/src/modes/controllers/selector-controller.ts +7 -0
- package/src/modes/interactive-mode.ts +47 -0
- package/src/modes/rpc/rpc-mode.ts +3 -3
- package/src/modes/runtime-init.ts +2 -1
- package/src/modes/types.ts +5 -0
- package/src/prompts/agents/designer.md +8 -0
- package/src/prompts/review-request.md +1 -1
- package/src/prompts/system/subagent-system-prompt.md +4 -1
- package/src/prompts/tools/eval.md +13 -3
- package/src/prompts/tools/irc.md +1 -1
- package/src/sdk.ts +9 -1
- package/src/session/agent-session.ts +260 -50
- package/src/session/messages.ts +1 -1
- package/src/session/session-manager.ts +3 -1
- package/src/slash-commands/builtin-registry.ts +5 -2
- package/src/system-prompt.ts +7 -1
- package/src/task/executor.ts +105 -8
- package/src/task/index.ts +70 -9
- package/src/tools/github-cache.ts +32 -7
- package/src/tools/job.ts +14 -1
- package/src/utils/lang-from-path.ts +5 -0
- package/src/utils/markit.ts +24 -1
- package/src/web/search/index.ts +2 -2
- package/src/web/search/provider.ts +14 -2
package/src/discovery/github.ts
CHANGED
|
@@ -5,11 +5,14 @@
|
|
|
5
5
|
* Priority: 30 (shared standard provider)
|
|
6
6
|
*
|
|
7
7
|
* Sources:
|
|
8
|
-
* - Project: .github/ (
|
|
8
|
+
* - Project: .github/ (repo-local Copilot config)
|
|
9
|
+
* - User: ~/.copilot/ (user-global Copilot CLI config; relocatable via COPILOT_HOME)
|
|
10
|
+
* - Extra: directories listed in COPILOT_CUSTOM_INSTRUCTIONS_DIRS
|
|
9
11
|
*
|
|
10
12
|
* Capabilities:
|
|
11
|
-
* - context-files: copilot-instructions.md in .github/
|
|
12
|
-
* -
|
|
13
|
+
* - context-files: copilot-instructions.md in .github/ and ~/.copilot/; AGENTS.md in each COPILOT_CUSTOM_INSTRUCTIONS_DIRS
|
|
14
|
+
* - rules: *.instructions.md under .github/instructions/ and <dir>/.github/instructions/ for each custom dir (applyTo frontmatter)
|
|
15
|
+
* - prompts: *.prompt.md in .github/prompts/ (VS Code Copilot prompt files)
|
|
13
16
|
* - skills: <name>/SKILL.md in .github/skills/ (GitHub Agent Skills layout)
|
|
14
17
|
*/
|
|
15
18
|
import * as path from "node:path";
|
|
@@ -18,10 +21,21 @@ import { registerProvider } from "../capability";
|
|
|
18
21
|
import { type ContextFile, contextFileCapability } from "../capability/context-file";
|
|
19
22
|
import { readFile } from "../capability/fs";
|
|
20
23
|
import { type Instruction, instructionCapability } from "../capability/instruction";
|
|
24
|
+
import { type Prompt, promptCapability } from "../capability/prompt";
|
|
25
|
+
import { type Rule, ruleCapability } from "../capability/rule";
|
|
21
26
|
import { type Skill, skillCapability } from "../capability/skill";
|
|
22
27
|
import type { LoadContext, LoadResult, SourceMeta } from "../capability/types";
|
|
23
28
|
|
|
24
|
-
import {
|
|
29
|
+
import {
|
|
30
|
+
buildRuleFromMarkdown,
|
|
31
|
+
calculateDepth,
|
|
32
|
+
createSourceMeta,
|
|
33
|
+
getProjectPath,
|
|
34
|
+
loadFilesFromDir,
|
|
35
|
+
parseCSV,
|
|
36
|
+
resolveCopilotHome,
|
|
37
|
+
scanSkillsFromDir,
|
|
38
|
+
} from "./helpers";
|
|
25
39
|
|
|
26
40
|
const PROVIDER_ID = "github";
|
|
27
41
|
const DISPLAY_NAME = "GitHub Copilot";
|
|
@@ -52,6 +66,33 @@ async function loadContextFiles(ctx: LoadContext): Promise<LoadResult<ContextFil
|
|
|
52
66
|
}
|
|
53
67
|
}
|
|
54
68
|
|
|
69
|
+
// User-global instructions (~/.copilot/copilot-instructions.md), applied across all repos.
|
|
70
|
+
const userInstructionsPath = path.join(resolveCopilotHome(ctx.home), "copilot-instructions.md");
|
|
71
|
+
const userContent = await readFile(userInstructionsPath);
|
|
72
|
+
if (userContent) {
|
|
73
|
+
items.push({
|
|
74
|
+
path: userInstructionsPath,
|
|
75
|
+
content: userContent,
|
|
76
|
+
level: "user",
|
|
77
|
+
_source: createSourceMeta(PROVIDER_ID, userInstructionsPath, "user"),
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Each COPILOT_CUSTOM_INSTRUCTIONS_DIRS entry contributes an AGENTS.md (Copilot CLI
|
|
82
|
+
// searches these dirs for AGENTS.md + .github/instructions/**; the latter is handled
|
|
83
|
+
// by loadInstructions). copilot-instructions.md is NOT part of the custom-dir spec.
|
|
84
|
+
for (const dir of copilotCustomInstructionDirs()) {
|
|
85
|
+
const agentsMdPath = path.join(dir, "AGENTS.md");
|
|
86
|
+
const agentsMdContent = await readFile(agentsMdPath);
|
|
87
|
+
if (agentsMdContent) {
|
|
88
|
+
items.push({
|
|
89
|
+
path: agentsMdPath,
|
|
90
|
+
content: agentsMdContent,
|
|
91
|
+
level: "user",
|
|
92
|
+
_source: createSourceMeta(PROVIDER_ID, agentsMdPath, "user"),
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
}
|
|
55
96
|
return { items, warnings };
|
|
56
97
|
}
|
|
57
98
|
|
|
@@ -65,9 +106,23 @@ async function loadInstructions(ctx: LoadContext): Promise<LoadResult<Instructio
|
|
|
65
106
|
|
|
66
107
|
const instructionsDir = getProjectPath(ctx, "github", "instructions");
|
|
67
108
|
if (instructionsDir) {
|
|
109
|
+
// Path-specific instructions live "within or below" .github/instructions/ → recurse.
|
|
68
110
|
const result = await loadFilesFromDir<Instruction>(ctx, instructionsDir, PROVIDER_ID, "project", {
|
|
69
111
|
extensions: ["md"],
|
|
70
112
|
transform: transformInstruction,
|
|
113
|
+
recursive: true,
|
|
114
|
+
});
|
|
115
|
+
items.push(...result.items);
|
|
116
|
+
if (result.warnings) warnings.push(...result.warnings);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Each COPILOT_CUSTOM_INSTRUCTIONS_DIRS entry contributes <dir>/.github/instructions/**/*.instructions.md.
|
|
120
|
+
for (const dir of copilotCustomInstructionDirs()) {
|
|
121
|
+
const customInstructionsDir = path.join(dir, ".github", "instructions");
|
|
122
|
+
const result = await loadFilesFromDir<Instruction>(ctx, customInstructionsDir, PROVIDER_ID, "user", {
|
|
123
|
+
extensions: ["md"],
|
|
124
|
+
transform: transformInstruction,
|
|
125
|
+
recursive: true,
|
|
71
126
|
});
|
|
72
127
|
items.push(...result.items);
|
|
73
128
|
if (result.warnings) warnings.push(...result.warnings);
|
|
@@ -99,6 +154,118 @@ function transformInstruction(name: string, content: string, filePath: string, s
|
|
|
99
154
|
};
|
|
100
155
|
}
|
|
101
156
|
|
|
157
|
+
// =============================================================================
|
|
158
|
+
// Rules
|
|
159
|
+
// =============================================================================
|
|
160
|
+
|
|
161
|
+
async function loadRules(ctx: LoadContext): Promise<LoadResult<Rule>> {
|
|
162
|
+
const items: Rule[] = [];
|
|
163
|
+
const warnings: string[] = [];
|
|
164
|
+
|
|
165
|
+
const load = async (dir: string, level: "user" | "project") => {
|
|
166
|
+
const applyToWarnings: string[] = [];
|
|
167
|
+
const result = await loadFilesFromDir<Rule>(ctx, dir, PROVIDER_ID, level, {
|
|
168
|
+
extensions: ["md"],
|
|
169
|
+
transform: (name, content, filePath, source) =>
|
|
170
|
+
transformInstructionRule(name, content, filePath, source, applyToWarnings),
|
|
171
|
+
recursive: true,
|
|
172
|
+
});
|
|
173
|
+
items.push(...result.items);
|
|
174
|
+
if (result.warnings) warnings.push(...result.warnings);
|
|
175
|
+
warnings.push(...applyToWarnings);
|
|
176
|
+
};
|
|
177
|
+
|
|
178
|
+
const instructionsDir = getProjectPath(ctx, "github", "instructions");
|
|
179
|
+
if (instructionsDir) {
|
|
180
|
+
await load(instructionsDir, "project");
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
for (const dir of copilotCustomInstructionDirs()) {
|
|
184
|
+
await load(path.join(dir, ".github", "instructions"), "user");
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
return { items, warnings };
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function transformInstructionRule(
|
|
191
|
+
name: string,
|
|
192
|
+
content: string,
|
|
193
|
+
filePath: string,
|
|
194
|
+
source: SourceMeta,
|
|
195
|
+
warnings: string[],
|
|
196
|
+
): Rule | null {
|
|
197
|
+
if (!name.endsWith(".instructions.md")) {
|
|
198
|
+
return null;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
const { frontmatter } = parseFrontmatter(content, { source: filePath });
|
|
202
|
+
const applyToGlobs = normalizeApplyToGlobs(frontmatter.applyTo);
|
|
203
|
+
if (!applyToGlobs) {
|
|
204
|
+
warnings.push(`Missing applyTo in ${filePath}; loaded without GitHub glob scoping.`);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
const rule = buildRuleFromMarkdown(name, content, filePath, source, {
|
|
208
|
+
stripNamePattern: /\.instructions\.md$/,
|
|
209
|
+
});
|
|
210
|
+
if (applyToGlobs?.some(isAlwaysApplyGlob)) {
|
|
211
|
+
return { ...rule, alwaysApply: true, globs: undefined };
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
const description = rule.description ?? describeInstructionRule(applyToGlobs);
|
|
215
|
+
return { ...rule, alwaysApply: false, globs: applyToGlobs, description };
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function normalizeApplyToGlobs(value: unknown): string[] | undefined {
|
|
219
|
+
// GitHub documents applyTo as a single comma-separated string (e.g.
|
|
220
|
+
// "**/*.ts,**/*.tsx"); also tolerate a YAML array of such strings.
|
|
221
|
+
const raw = Array.isArray(value) ? value : [value];
|
|
222
|
+
const globs = raw.flatMap(item => (typeof item === "string" ? parseCSV(item) : []));
|
|
223
|
+
return globs.length > 0 ? globs : undefined;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
function isAlwaysApplyGlob(glob: string): boolean {
|
|
227
|
+
// GitHub treats "*", "**", and "**/*" as matching every file.
|
|
228
|
+
return glob === "*" || glob === "**" || glob === "**/*";
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
function describeInstructionRule(globs: string[] | undefined): string {
|
|
232
|
+
if (!globs) return "GitHub Copilot instructions without applyTo metadata";
|
|
233
|
+
return `GitHub Copilot instructions for ${globs.join(", ")}`;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// =============================================================================
|
|
237
|
+
// Prompts
|
|
238
|
+
// =============================================================================
|
|
239
|
+
|
|
240
|
+
async function loadPrompts(ctx: LoadContext): Promise<LoadResult<Prompt>> {
|
|
241
|
+
// `.github/prompts/*.prompt.md` is the VS Code Copilot prompt-file convention (the
|
|
242
|
+
// Copilot CLI has no prompt-file feature of its own); surface them as slash commands.
|
|
243
|
+
const promptsDir = getProjectPath(ctx, "github", "prompts");
|
|
244
|
+
if (!promptsDir) return { items: [], warnings: [] };
|
|
245
|
+
|
|
246
|
+
return loadFilesFromDir<Prompt>(ctx, promptsDir, PROVIDER_ID, "project", {
|
|
247
|
+
extensions: ["md"],
|
|
248
|
+
transform: transformPrompt,
|
|
249
|
+
});
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
function transformPrompt(name: string, content: string, filePath: string, source: SourceMeta): Prompt | null {
|
|
253
|
+
// Prompt files are `*.prompt.md`; ignore other markdown that may share the dir.
|
|
254
|
+
if (!name.endsWith(".prompt.md")) return null;
|
|
255
|
+
|
|
256
|
+
const { frontmatter, body } = parseFrontmatter(content, { source: filePath });
|
|
257
|
+
const promptName =
|
|
258
|
+
typeof frontmatter.name === "string" && frontmatter.name ? frontmatter.name : path.basename(name, ".prompt.md");
|
|
259
|
+
|
|
260
|
+
return { name: promptName, path: filePath, content: body, _source: source };
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/** Directories listed in the COPILOT_CUSTOM_INSTRUCTIONS_DIRS env var (comma-separated). */
|
|
264
|
+
function copilotCustomInstructionDirs(): string[] {
|
|
265
|
+
const raw = process.env.COPILOT_CUSTOM_INSTRUCTIONS_DIRS;
|
|
266
|
+
return raw ? parseCSV(raw) : [];
|
|
267
|
+
}
|
|
268
|
+
|
|
102
269
|
// =============================================================================
|
|
103
270
|
// Skills
|
|
104
271
|
// =============================================================================
|
|
@@ -132,7 +299,8 @@ async function loadSkills(ctx: LoadContext): Promise<LoadResult<Skill>> {
|
|
|
132
299
|
registerProvider(contextFileCapability.id, {
|
|
133
300
|
id: PROVIDER_ID,
|
|
134
301
|
displayName: DISPLAY_NAME,
|
|
135
|
-
description:
|
|
302
|
+
description:
|
|
303
|
+
"Load copilot-instructions.md from .github/ and ~/.copilot/; AGENTS.md from COPILOT_CUSTOM_INSTRUCTIONS_DIRS",
|
|
136
304
|
priority: PRIORITY,
|
|
137
305
|
load: loadContextFiles,
|
|
138
306
|
});
|
|
@@ -140,11 +308,18 @@ registerProvider(contextFileCapability.id, {
|
|
|
140
308
|
registerProvider(instructionCapability.id, {
|
|
141
309
|
id: PROVIDER_ID,
|
|
142
310
|
displayName: DISPLAY_NAME,
|
|
143
|
-
description: "Load *.instructions.md from .github/instructions/
|
|
311
|
+
description: "Load *.instructions.md from .github/instructions/ and COPILOT_CUSTOM_INSTRUCTIONS_DIRS",
|
|
144
312
|
priority: PRIORITY,
|
|
145
313
|
load: loadInstructions,
|
|
146
314
|
});
|
|
147
315
|
|
|
316
|
+
registerProvider<Rule>(ruleCapability.id, {
|
|
317
|
+
id: PROVIDER_ID,
|
|
318
|
+
displayName: DISPLAY_NAME,
|
|
319
|
+
description: "Load *.instructions.md from .github/instructions/ as Copilot-scoped rules",
|
|
320
|
+
priority: PRIORITY,
|
|
321
|
+
load: loadRules,
|
|
322
|
+
});
|
|
148
323
|
registerProvider<Skill>(skillCapability.id, {
|
|
149
324
|
id: PROVIDER_ID,
|
|
150
325
|
displayName: DISPLAY_NAME,
|
|
@@ -152,3 +327,11 @@ registerProvider<Skill>(skillCapability.id, {
|
|
|
152
327
|
priority: PRIORITY,
|
|
153
328
|
load: loadSkills,
|
|
154
329
|
});
|
|
330
|
+
|
|
331
|
+
registerProvider<Prompt>(promptCapability.id, {
|
|
332
|
+
id: PROVIDER_ID,
|
|
333
|
+
displayName: DISPLAY_NAME,
|
|
334
|
+
description: "Load *.prompt.md from .github/prompts/ (VS Code Copilot prompt files)",
|
|
335
|
+
priority: PRIORITY,
|
|
336
|
+
load: loadPrompts,
|
|
337
|
+
});
|
package/src/discovery/helpers.ts
CHANGED
|
@@ -107,6 +107,17 @@ export function getProjectPath(ctx: LoadContext, source: SourceId, subpath: stri
|
|
|
107
107
|
return path.join(ctx.cwd, paths.projectDir, subpath);
|
|
108
108
|
}
|
|
109
109
|
|
|
110
|
+
/**
|
|
111
|
+
* Resolve GitHub Copilot CLI's user-global config root. Copilot stores per-user
|
|
112
|
+
* instructions/prompts/agents/MCP under `~/.copilot`, relocatable via the
|
|
113
|
+
* `COPILOT_HOME` env var (mirrors Copilot CLI's `--config-dir`). Falls back to
|
|
114
|
+
* `<home>/.copilot` when the override is unset.
|
|
115
|
+
*/
|
|
116
|
+
export function resolveCopilotHome(home: string): string {
|
|
117
|
+
const override = process.env.COPILOT_HOME?.trim();
|
|
118
|
+
return override ? override : path.join(home, ".copilot");
|
|
119
|
+
}
|
|
120
|
+
|
|
110
121
|
/**
|
|
111
122
|
* Create source metadata for an item.
|
|
112
123
|
*/
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { describe, expect, it } from "bun:test";
|
|
2
|
+
import * as vm from "node:vm";
|
|
3
|
+
import { JAVASCRIPT_PRELUDE_SOURCE } from "../js/shared/prelude";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* The eval `agent()` helper grows a `returnHandle` option that turns its bare
|
|
7
|
+
* text result into a DAG node dict carrying the spawned agent's recoverable
|
|
8
|
+
* `agent://<id>` handle, so a downstream `pipeline`/`parallel` stage can wire
|
|
9
|
+
* the transcript by reference instead of re-inlining it. These lock the node
|
|
10
|
+
* shape, backward compatibility of the default path, the schema interaction,
|
|
11
|
+
* and the no-`details` fallback (the helper must never throw).
|
|
12
|
+
*
|
|
13
|
+
* The prelude source is executed verbatim in a throwaway VM context with only
|
|
14
|
+
* the host bridge (`__omp_call_tool__`) stubbed — no worker, no kernel — so the
|
|
15
|
+
* test runs against the real shipped helper, not a re-implementation.
|
|
16
|
+
*/
|
|
17
|
+
function loadPrelude(callTool: (name: string, args: unknown) => Promise<unknown>): Record<string, unknown> {
|
|
18
|
+
const sandbox: Record<string, unknown> = { __omp_call_tool__: callTool };
|
|
19
|
+
vm.createContext(sandbox);
|
|
20
|
+
vm.runInContext(JAVASCRIPT_PRELUDE_SOURCE, sandbox);
|
|
21
|
+
return sandbox;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
type AgentHelper = (prompt: string, opts?: Record<string, unknown>) => Promise<unknown>;
|
|
25
|
+
|
|
26
|
+
describe("eval js agent() returnHandle", () => {
|
|
27
|
+
it("returns a DAG node carrying the agent:// handle when returnHandle is set", async () => {
|
|
28
|
+
let seenName: string | undefined;
|
|
29
|
+
const sandbox = loadPrelude(async name => {
|
|
30
|
+
seenName = name;
|
|
31
|
+
return { text: "hello world", details: { agent: "task", id: "abc123", model: "m", structured: false } };
|
|
32
|
+
});
|
|
33
|
+
const node = await (sandbox.agent as AgentHelper)("say hi", { returnHandle: true });
|
|
34
|
+
expect(seenName).toBe("__agent__");
|
|
35
|
+
expect(node).toEqual({
|
|
36
|
+
text: "hello world",
|
|
37
|
+
output: "hello world",
|
|
38
|
+
handle: "agent://abc123",
|
|
39
|
+
id: "abc123",
|
|
40
|
+
agent: "task",
|
|
41
|
+
});
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it("returns bare text by default (backward compatible)", async () => {
|
|
45
|
+
const sandbox = loadPrelude(async () => ({
|
|
46
|
+
text: "hello world",
|
|
47
|
+
details: { agent: "task", id: "abc123", structured: false },
|
|
48
|
+
}));
|
|
49
|
+
const out = await (sandbox.agent as AgentHelper)("say hi");
|
|
50
|
+
expect(out).toBe("hello world");
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it("carries the parsed object under data when schema and returnHandle combine", async () => {
|
|
54
|
+
const payload = JSON.stringify({ k: 1 });
|
|
55
|
+
const sandbox = loadPrelude(async () => ({
|
|
56
|
+
text: payload,
|
|
57
|
+
details: { agent: "task", id: "id-9", structured: true },
|
|
58
|
+
}));
|
|
59
|
+
const node = (await (sandbox.agent as AgentHelper)("emit", {
|
|
60
|
+
schema: { type: "object" },
|
|
61
|
+
returnHandle: true,
|
|
62
|
+
})) as Record<string, unknown>;
|
|
63
|
+
expect(node.handle).toBe("agent://id-9");
|
|
64
|
+
expect(node.data).toEqual({ k: 1 });
|
|
65
|
+
expect(node.text).toBe(payload);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it("falls back to a null handle without throwing when the bridge omits details", async () => {
|
|
69
|
+
const sandbox = loadPrelude(async () => ({ text: "lonely" }));
|
|
70
|
+
const node = await (sandbox.agent as AgentHelper)("x", { returnHandle: true });
|
|
71
|
+
expect(node).toEqual({ text: "lonely", output: "lonely", handle: null, id: null, agent: null });
|
|
72
|
+
});
|
|
73
|
+
});
|
|
@@ -117,10 +117,19 @@ if (!globalThis.__omp_js_prelude_loaded__) {
|
|
|
117
117
|
};
|
|
118
118
|
|
|
119
119
|
const agent = async (prompt, opts, ...rest) => {
|
|
120
|
-
const o = optionsArg("agent", opts, rest, ["agentType", "model", "label", "schema"], "{ agentType, model, label, schema }");
|
|
121
|
-
const
|
|
120
|
+
const o = optionsArg("agent", opts, rest, ["agentType", "model", "label", "schema"], "{ agentType, model, label, schema, returnHandle }");
|
|
121
|
+
const { returnHandle, ...callArgs } = o;
|
|
122
|
+
const res = await globalThis.__omp_call_tool__("__agent__", { prompt, ...callArgs });
|
|
122
123
|
const text = res && typeof res === "object" ? res.text : res;
|
|
123
|
-
|
|
124
|
+
const parsed = hasOwn(callArgs, "schema") ? JSON.parse(text) : text;
|
|
125
|
+
if (!returnHandle) return parsed;
|
|
126
|
+
const details = res && typeof res === "object" ? res.details : undefined;
|
|
127
|
+
if (!details || typeof details !== "object" || details.id == null) {
|
|
128
|
+
return { text, output: text, handle: null, id: null, agent: null };
|
|
129
|
+
}
|
|
130
|
+
const node = { text, output: text, handle: `agent://${details.id}`, id: details.id, agent: details.agent ?? null };
|
|
131
|
+
if (hasOwn(callArgs, "schema")) node.data = parsed;
|
|
132
|
+
return node;
|
|
124
133
|
};
|
|
125
134
|
|
|
126
135
|
// Pool ceiling mirrors the task tool's `task.maxConcurrency` setting so an
|
package/src/eval/py/prelude.py
CHANGED
|
@@ -519,7 +519,7 @@ if "__omp_prelude_loaded__" not in globals():
|
|
|
519
519
|
text = res.get("text") if isinstance(res, dict) else res
|
|
520
520
|
return json.loads(text) if schema is not None else text
|
|
521
521
|
|
|
522
|
-
def agent(prompt, *, agent_type="task", model=None, label=None, schema=None):
|
|
522
|
+
def agent(prompt, *, agent_type="task", model=None, label=None, schema=None, return_handle=False):
|
|
523
523
|
"""Run a subagent and return its final output.
|
|
524
524
|
|
|
525
525
|
`agent_type` selects the subagent definition (default "task"). Pass
|
|
@@ -527,6 +527,15 @@ if "__omp_prelude_loaded__" not in globals():
|
|
|
527
527
|
id, and `schema` to request structured JSON output; when `schema` is
|
|
528
528
|
supplied the parsed object is returned. Share background by writing a
|
|
529
529
|
local:// file and referencing it in the prompt.
|
|
530
|
+
|
|
531
|
+
Set `return_handle=True` to receive a DAG node dict instead of bare
|
|
532
|
+
text: ``{"text", "output", "handle", "id", "agent"}`` where ``handle``
|
|
533
|
+
is the spawned agent's recoverable ``agent://<id>`` URI. A downstream
|
|
534
|
+
``pipeline``/``parallel`` stage embeds that ``handle`` (or ``output``)
|
|
535
|
+
in its prompt so a large transcript flows through the graph by
|
|
536
|
+
reference, never re-inlined. When ``schema`` is also set the parsed
|
|
537
|
+
object lands under ``"data"``. If the bridge returns no recoverable id
|
|
538
|
+
the node still resolves with ``handle=None`` — the helper never throws.
|
|
530
539
|
"""
|
|
531
540
|
args = {"prompt": prompt}
|
|
532
541
|
if agent_type is not None:
|
|
@@ -539,7 +548,22 @@ if "__omp_prelude_loaded__" not in globals():
|
|
|
539
548
|
args["schema"] = schema
|
|
540
549
|
res = _bridge_call("__agent__", args)
|
|
541
550
|
text = res.get("text") if isinstance(res, dict) else res
|
|
542
|
-
|
|
551
|
+
parsed = json.loads(text) if schema is not None else text
|
|
552
|
+
if not return_handle:
|
|
553
|
+
return parsed
|
|
554
|
+
details = res.get("details") if isinstance(res, dict) else None
|
|
555
|
+
if not isinstance(details, dict) or details.get("id") is None:
|
|
556
|
+
return {"text": text, "output": text, "handle": None, "id": None, "agent": None}
|
|
557
|
+
node = {
|
|
558
|
+
"text": text,
|
|
559
|
+
"output": text,
|
|
560
|
+
"handle": f"agent://{details['id']}",
|
|
561
|
+
"id": details["id"],
|
|
562
|
+
"agent": details.get("agent"),
|
|
563
|
+
}
|
|
564
|
+
if schema is not None:
|
|
565
|
+
node["data"] = parsed
|
|
566
|
+
return node
|
|
543
567
|
|
|
544
568
|
def _concurrency_limit():
|
|
545
569
|
"""Worker-pool ceiling from the host ``task.maxConcurrency`` setting.
|
|
@@ -11,7 +11,7 @@ import { Settings, type ShellMinimizerSettings } from "../config/settings";
|
|
|
11
11
|
import { OutputSink } from "../session/streaming-output";
|
|
12
12
|
import { resolveOutputMaxColumns, resolveOutputSinkHeadBytes } from "../tools/output-meta";
|
|
13
13
|
import { getOrCreateSnapshot } from "../utils/shell-snapshot";
|
|
14
|
-
import {
|
|
14
|
+
import { buildNonInteractiveEnv } from "./non-interactive-env";
|
|
15
15
|
|
|
16
16
|
export interface BashExecutorOptions {
|
|
17
17
|
cwd?: string;
|
|
@@ -184,7 +184,7 @@ export async function executeBash(command: string, options?: BashExecutorOptions
|
|
|
184
184
|
const minimizer = buildMinimizerOptions(settings.getGroup("shellMinimizer"));
|
|
185
185
|
|
|
186
186
|
const commandCwd = await resolveShellCwd(options?.cwd);
|
|
187
|
-
const commandEnv = options?.env
|
|
187
|
+
const commandEnv = buildNonInteractiveEnv(options?.env);
|
|
188
188
|
|
|
189
189
|
// Apply command prefix if configured
|
|
190
190
|
const prefixedCommand = prefix ? `${prefix} ${command}` : command;
|
|
@@ -46,3 +46,74 @@ export const NON_INTERACTIVE_ENV: Readonly<Record<string, string>> = {
|
|
|
46
46
|
COMPOSER_NO_INTERACTION: "1",
|
|
47
47
|
CLOUDSDK_CORE_DISABLE_PROMPTS: "1",
|
|
48
48
|
};
|
|
49
|
+
|
|
50
|
+
const WINDOWS_UTF8_ENV_DEFAULT_GROUPS: ReadonlyArray<ReadonlyArray<readonly [key: string, value: string]>> = [
|
|
51
|
+
[
|
|
52
|
+
["PYTHONIOENCODING", "utf-8"],
|
|
53
|
+
["PYTHONUTF8", "1"],
|
|
54
|
+
],
|
|
55
|
+
[
|
|
56
|
+
["LANG", "C.UTF-8"],
|
|
57
|
+
["LC_ALL", "C.UTF-8"],
|
|
58
|
+
],
|
|
59
|
+
];
|
|
60
|
+
|
|
61
|
+
function hasEnvValue(
|
|
62
|
+
env: Record<string, string | undefined> | undefined,
|
|
63
|
+
key: string,
|
|
64
|
+
platform: NodeJS.Platform,
|
|
65
|
+
): boolean {
|
|
66
|
+
if (!env) return false;
|
|
67
|
+
if (platform !== "win32") return env[key] !== undefined;
|
|
68
|
+
|
|
69
|
+
for (const [existingKey, value] of Object.entries(env)) {
|
|
70
|
+
if (value !== undefined && existingKey.toLowerCase() === key.toLowerCase()) {
|
|
71
|
+
return true;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
return false;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function hasLocaleEnvValue(env: Record<string, string | undefined> | undefined, platform: NodeJS.Platform): boolean {
|
|
78
|
+
if (!env) return false;
|
|
79
|
+
for (const [key, value] of Object.entries(env)) {
|
|
80
|
+
if (value === undefined) continue;
|
|
81
|
+
const normalizedKey = platform === "win32" ? key.toUpperCase() : key;
|
|
82
|
+
if (normalizedKey === "LANG" || normalizedKey.startsWith("LC_")) return true;
|
|
83
|
+
}
|
|
84
|
+
return false;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function hasEnvGroupValue(
|
|
88
|
+
env: Record<string, string | undefined> | undefined,
|
|
89
|
+
group: ReadonlyArray<readonly [key: string, value: string]>,
|
|
90
|
+
platform: NodeJS.Platform,
|
|
91
|
+
): boolean {
|
|
92
|
+
if (group.some(([key]) => key === "LC_ALL") && hasLocaleEnvValue(env, platform)) return true;
|
|
93
|
+
for (const [key] of group) {
|
|
94
|
+
if (hasEnvValue(env, key, platform)) return true;
|
|
95
|
+
}
|
|
96
|
+
return false;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** Builds the per-command environment for non-interactive child processes. */
|
|
100
|
+
export function buildNonInteractiveEnv(
|
|
101
|
+
overrides?: Record<string, string>,
|
|
102
|
+
baseEnv: Record<string, string | undefined> = Bun.env,
|
|
103
|
+
platform: NodeJS.Platform = process.platform,
|
|
104
|
+
): Record<string, string> {
|
|
105
|
+
if (platform !== "win32") {
|
|
106
|
+
return overrides ? { ...NON_INTERACTIVE_ENV, ...overrides } : NON_INTERACTIVE_ENV;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const env: Record<string, string> = { ...NON_INTERACTIVE_ENV };
|
|
110
|
+
for (const group of WINDOWS_UTF8_ENV_DEFAULT_GROUPS) {
|
|
111
|
+
if (hasEnvGroupValue(baseEnv, group, platform) || hasEnvGroupValue(overrides, group, platform)) {
|
|
112
|
+
continue;
|
|
113
|
+
}
|
|
114
|
+
for (const [key, value] of group) {
|
|
115
|
+
env[key] = value;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
return overrides ? { ...env, ...overrides } : env;
|
|
119
|
+
}
|