@arnilo/prism 0.0.1 → 0.0.3
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 +19 -2
- package/README.md +17 -7
- package/dist/agent-definitions.d.ts +12 -0
- package/dist/agent-definitions.js +131 -0
- package/dist/agent-loops.d.ts +14 -0
- package/dist/agent-loops.js +161 -0
- package/dist/agents.js +263 -76
- package/dist/cache-helpers.d.ts +28 -0
- package/dist/cache-helpers.js +73 -0
- package/dist/cli-runner.d.ts +38 -2
- package/dist/cli-runner.js +167 -5
- package/dist/compaction.js +2 -0
- package/dist/config.js +47 -12
- package/dist/contracts.d.ts +581 -6
- package/dist/contracts.js +41 -1
- package/dist/contribution-parsing.d.ts +19 -0
- package/dist/contribution-parsing.js +124 -0
- package/dist/contributions.d.ts +13 -3
- package/dist/contributions.js +96 -20
- package/dist/extensions.js +3 -0
- package/dist/index.d.ts +19 -9
- package/dist/index.js +10 -4
- package/dist/input.d.ts +7 -1
- package/dist/input.js +52 -11
- package/dist/instruction-injection.d.ts +28 -0
- package/dist/instruction-injection.js +55 -0
- package/dist/manifests.d.ts +1 -1
- package/dist/manifests.js +3 -3
- package/dist/models.d.ts +4 -1
- package/dist/models.js +5 -2
- package/dist/node/agent-definitions.d.ts +98 -0
- package/dist/node/agent-definitions.js +389 -0
- package/dist/node/contribution-discovery.d.ts +17 -0
- package/dist/node/contribution-discovery.js +163 -0
- package/dist/node/instruction-injectors.d.ts +32 -0
- package/dist/node/instruction-injectors.js +72 -0
- package/dist/node/session-store-jsonl.d.ts +1 -1
- package/dist/node/session-store-jsonl.js +42 -4
- package/dist/node/system-project-prompts.d.ts +30 -0
- package/dist/node/system-project-prompts.js +53 -0
- package/dist/provider-events.d.ts +3 -1
- package/dist/provider-events.js +34 -0
- package/dist/provider-request-policy.js +15 -1
- package/dist/providers/openai-compatible.js +1 -1
- package/dist/providers.d.ts +6 -2
- package/dist/providers.js +15 -1
- package/dist/redaction.d.ts +2 -1
- package/dist/redaction.js +3 -0
- package/dist/registry-options.d.ts +5 -0
- package/dist/registry-options.js +5 -0
- package/dist/rpc.d.ts +6 -2
- package/dist/rpc.js +71 -13
- package/dist/session-stores.d.ts +3 -1
- package/dist/session-stores.js +67 -6
- package/dist/skills.d.ts +4 -1
- package/dist/skills.js +3 -1
- package/dist/system-prompts.js +6 -2
- package/dist/testing/compaction-conformance.d.ts +17 -0
- package/dist/testing/compaction-conformance.js +61 -0
- package/dist/testing/extension-conformance.d.ts +26 -0
- package/dist/testing/extension-conformance.js +55 -0
- package/dist/testing/provider-conformance.d.ts +7 -0
- package/dist/testing/provider-conformance.js +18 -31
- package/dist/testing/session-store-conformance.d.ts +20 -0
- package/dist/testing/session-store-conformance.js +92 -0
- package/dist/testing/tool-conformance.d.ts +39 -0
- package/dist/testing/tool-conformance.js +79 -0
- package/dist/tools.d.ts +7 -2
- package/dist/tools.js +50 -13
- package/docs/agent-definitions.md +251 -0
- package/docs/agent-events.md +199 -0
- package/docs/agent-loops.md +217 -0
- package/docs/agent-session-runtime.md +20 -8
- package/docs/cli-rpc.md +39 -4
- package/docs/coding-agent-tools.md +208 -0
- package/docs/compaction-and-retry.md +2 -2
- package/docs/compaction-conformance.md +76 -0
- package/docs/compaction-llm.md +6 -3
- package/docs/compaction-observational-memory.md +4 -4
- package/docs/configuration-and-manifests.md +6 -1
- package/docs/context-and-skills.md +79 -6
- package/docs/contribution-discovery.md +149 -0
- package/docs/contribution-registries.md +9 -6
- package/docs/credentials-and-redaction.md +2 -0
- package/docs/customization.md +191 -0
- package/docs/database-persistence.md +407 -0
- package/docs/extension-authoring.md +193 -0
- package/docs/extension-conformance.md +80 -0
- package/docs/extensions.md +6 -0
- package/docs/host-security.md +141 -0
- package/docs/index.md +41 -19
- package/docs/input-and-prompt-assembly.md +19 -3
- package/docs/instruction-injection.md +183 -0
- package/docs/migration.md +201 -0
- package/docs/model-registry.md +122 -0
- package/docs/node-jsonl-session-store.md +5 -4
- package/docs/performance.md +127 -0
- package/docs/provider-caching.md +206 -0
- package/docs/provider-conformance.md +32 -5
- package/docs/provider-layer.md +51 -11
- package/docs/provider-packages.md +65 -5
- package/docs/provider-request-policies.md +113 -0
- package/docs/providers/kimi.md +22 -0
- package/docs/providers/neuralwatt.md +388 -0
- package/docs/providers/openai-compatible.md +1 -0
- package/docs/providers/openai.md +21 -0
- package/docs/providers/opencode-go.md +31 -3
- package/docs/providers/openrouter.md +29 -0
- package/docs/providers/zai.md +17 -0
- package/docs/public-contracts.md +87 -12
- package/docs/release-and-install.md +79 -27
- package/docs/runs-and-usage.md +236 -0
- package/docs/session-store-conformance.md +78 -0
- package/docs/session-stores-and-branching.md +10 -6
- package/docs/session-stores.md +126 -0
- package/docs/settings-auth-trust-security.md +18 -4
- package/docs/structured-output.md +247 -0
- package/docs/system-prompts.md +104 -2
- package/docs/tool-conformance.md +87 -0
- package/docs/tools.md +65 -8
- package/package.json +36 -2
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
function throwIfAborted(signal) {
|
|
2
|
+
if (signal?.aborted)
|
|
3
|
+
throw new Error("Instruction injector aborted");
|
|
4
|
+
}
|
|
5
|
+
function shouldApply(contribution, ctx) {
|
|
6
|
+
if (contribution.when === "first_turn")
|
|
7
|
+
return ctx.turn === 1;
|
|
8
|
+
if (contribution.when === "every_turn")
|
|
9
|
+
return true;
|
|
10
|
+
// on_input: predicate optional, absent = apply every turn (default).
|
|
11
|
+
return contribution.predicate ? contribution.predicate(ctx) : true;
|
|
12
|
+
}
|
|
13
|
+
/** Run each selected injector against `ctx`, returning the matching contributions as
|
|
14
|
+
* `package`-source `SystemPromptContribution[]` (for `composeSystemPrompt`) and
|
|
15
|
+
* `ContextBlock[]` (for the context merge). Aborts on `ctx.signal`. Non-matching
|
|
16
|
+
* injectors contribute nothing; only `instructions`/`contextBlocks` are honored —
|
|
17
|
+
* injectors cannot register tools, skills, or permissions. */
|
|
18
|
+
export function runInstructionInjectors(injectors, ctx) {
|
|
19
|
+
const instructions = [];
|
|
20
|
+
const contextBlocks = [];
|
|
21
|
+
for (const injector of injectors) {
|
|
22
|
+
throwIfAborted(ctx.signal);
|
|
23
|
+
const contribution = injector.apply(ctx);
|
|
24
|
+
if (!shouldApply(contribution, ctx))
|
|
25
|
+
continue;
|
|
26
|
+
// ponytail: only instructions/contextBlocks honored; other fields grant nothing.
|
|
27
|
+
if (contribution.instructions) {
|
|
28
|
+
instructions.push({ id: `injector:${injector.name}`, source: "package", mode: "append", text: contribution.instructions });
|
|
29
|
+
}
|
|
30
|
+
if (contribution.contextBlocks)
|
|
31
|
+
contextBlocks.push(...contribution.contextBlocks);
|
|
32
|
+
}
|
|
33
|
+
return { instructions, contextBlocks };
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Resolve the effective injector list for a run. Mirrors {@link resolveActiveSkills}
|
|
37
|
+
* fail-closed name resolution. When `configured` is provided it wins (RunOptions
|
|
38
|
+
* supplies an explicit list); otherwise names are resolved against `registry`.
|
|
39
|
+
* An unknown name throws `Unknown instruction injector: <name>`.
|
|
40
|
+
*
|
|
41
|
+
* ponytail: no toolNames enforcement — injectors grant no tools (unlike skills).
|
|
42
|
+
*/
|
|
43
|
+
export function resolveInstructionInjectors(options) {
|
|
44
|
+
if (options.configured)
|
|
45
|
+
return options.configured;
|
|
46
|
+
if (!options.names || !options.registry)
|
|
47
|
+
return [];
|
|
48
|
+
return options.names.map((name) => {
|
|
49
|
+
const injector = options.registry.get(name);
|
|
50
|
+
if (!injector)
|
|
51
|
+
throw new Error(`Unknown instruction injector: ${name}`);
|
|
52
|
+
return injector;
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
//# sourceMappingURL=instruction-injection.js.map
|
package/dist/manifests.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { JsonObject } from "./contracts.js";
|
|
2
|
-
export type ManifestContributionKind = "provider" | "model" | "tool" | "contextProvider" | "skill" | "command" | "agent" | "inputBuilder" | "promptBuilder" | "compactionStrategy" | "retryPolicy" | "storeFactory" | "resourceLoader" | "settingsProvider" | "credentialResolver" | "providerPackage" | "authMethod" | "providerRequestPolicy" | "systemPromptContribution";
|
|
2
|
+
export type ManifestContributionKind = "provider" | "model" | "tool" | "contextProvider" | "skill" | "command" | "agent" | "inputBuilder" | "promptBuilder" | "compactionStrategy" | "retryPolicy" | "storeFactory" | "resourceLoader" | "settingsProvider" | "credentialResolver" | "providerPackage" | "authMethod" | "providerRequestPolicy" | "systemPromptContribution" | "instructionInjector";
|
|
3
3
|
export interface ManifestContributionDeclaration {
|
|
4
4
|
readonly kind: ManifestContributionKind;
|
|
5
5
|
readonly name: string;
|
package/dist/manifests.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { assertJsonObject
|
|
1
|
+
import { assertJsonObject } from "./config.js";
|
|
2
2
|
export function definePrismManifest(manifest) {
|
|
3
3
|
return parsePrismManifest(manifest);
|
|
4
4
|
}
|
|
@@ -77,6 +77,7 @@ function readKind(value, index) {
|
|
|
77
77
|
"authMethod",
|
|
78
78
|
"providerRequestPolicy",
|
|
79
79
|
"systemPromptContribution",
|
|
80
|
+
"instructionInjector",
|
|
80
81
|
]);
|
|
81
82
|
if (typeof value !== "string" || !kinds.has(value)) {
|
|
82
83
|
throw new Error(`manifest.contributions[${index}].kind must be a known contribution kind`);
|
|
@@ -101,8 +102,7 @@ function readOptionalJsonObject(record, key, label = `manifest.${key}`) {
|
|
|
101
102
|
const value = record[key];
|
|
102
103
|
if (value === undefined)
|
|
103
104
|
return {};
|
|
104
|
-
|
|
105
|
-
throw new Error(`${label} must be a JSON object`);
|
|
105
|
+
assertJsonObject(value, label);
|
|
106
106
|
return { [key]: value };
|
|
107
107
|
}
|
|
108
108
|
//# sourceMappingURL=manifests.js.map
|
package/dist/models.d.ts
CHANGED
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
import type { ModelConfig } from "./contracts.js";
|
|
2
|
+
import { type DuplicateRegistrationOptions } from "./registry-options.js";
|
|
2
3
|
export interface ModelRegistry {
|
|
3
4
|
register(model: ModelConfig): void;
|
|
4
5
|
get(provider: string, model: string): ModelConfig | undefined;
|
|
5
6
|
resolve(provider: string, model: string): ModelConfig;
|
|
6
7
|
list(): readonly ModelConfig[];
|
|
7
8
|
}
|
|
8
|
-
export
|
|
9
|
+
export interface ModelRegistryOptions extends DuplicateRegistrationOptions {
|
|
10
|
+
}
|
|
11
|
+
export declare function createModelRegistry(models?: readonly ModelConfig[], options?: ModelRegistryOptions): ModelRegistry;
|
package/dist/models.js
CHANGED
|
@@ -1,9 +1,12 @@
|
|
|
1
|
-
|
|
1
|
+
import { assertCanRegister } from "./registry-options.js";
|
|
2
|
+
export function createModelRegistry(models = [], options = {}) {
|
|
2
3
|
const byId = new Map();
|
|
3
4
|
const key = (provider, model) => `${provider}\0${model}`;
|
|
4
5
|
const registry = {
|
|
5
6
|
register(model) {
|
|
6
|
-
|
|
7
|
+
const id = key(model.provider, model.model);
|
|
8
|
+
assertCanRegister(byId, id, "model", `${model.provider}/${model.model}`, options.duplicate);
|
|
9
|
+
byId.set(id, model);
|
|
7
10
|
},
|
|
8
11
|
get(provider, model) {
|
|
9
12
|
return byId.get(key(provider, model));
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import type { Agent, AgentDefinitionResolutionContext, ContextProvider, DiscoveredContribution, ProviderResolver, ToolDefinition } from "../contracts.js";
|
|
2
|
+
import type { PermissionPolicy, TrustPolicy } from "../security.js";
|
|
3
|
+
/** Options for {@link resolveAgentBundle}. */
|
|
4
|
+
export interface ResolveAgentBundleOptions {
|
|
5
|
+
/** File reader. Defaults to `node:fs/promises.readFile`. */
|
|
6
|
+
readonly readFile?: (path: string) => Promise<string>;
|
|
7
|
+
/** Workspace root for the repo-level project prompt (`AGENTS.md`) and repo contributions. */
|
|
8
|
+
readonly workspaceRoot?: string;
|
|
9
|
+
/** Repo-level contributions from {@link discoverContributions}. */
|
|
10
|
+
readonly repoContributions?: readonly DiscoveredContribution[];
|
|
11
|
+
/** Host registries (models, providers, context providers). */
|
|
12
|
+
readonly registries?: AgentDefinitionResolutionContext["registries"];
|
|
13
|
+
/** Provider resolver override. */
|
|
14
|
+
readonly providerSource?: ProviderResolver;
|
|
15
|
+
/** Tool scope override. */
|
|
16
|
+
readonly tools?: AgentDefinitionResolutionContext["tools"];
|
|
17
|
+
/** Skill registry override. */
|
|
18
|
+
readonly skillsRegistry?: AgentDefinitionResolutionContext["skillsRegistry"];
|
|
19
|
+
/** Migration-only: omitted `tools`/`skills` activate every in-scope tool/skill. Defaults to fail-closed. */
|
|
20
|
+
readonly activateAllCapabilities?: AgentDefinitionResolutionContext["activateAllCapabilities"];
|
|
21
|
+
/** Final config overrides applied after the bundle is resolved. */
|
|
22
|
+
readonly overrides?: AgentDefinitionResolutionContext["overrides"];
|
|
23
|
+
/** Trust policy gating the app-config root (`SYSTEM.md`) and workspace root (`AGENTS.md`) independently. */
|
|
24
|
+
readonly trust?: TrustPolicy;
|
|
25
|
+
/** Permission policy asserting each prompt-file read. */
|
|
26
|
+
readonly permission?: PermissionPolicy;
|
|
27
|
+
/** Scope inclusion flags. All sources default to `true`. */
|
|
28
|
+
readonly include?: AgentBundleScopeFlags;
|
|
29
|
+
}
|
|
30
|
+
/** Which scopes contribute to a {@link resolveAgentBundle} call. */
|
|
31
|
+
export interface AgentBundleScopeFlags {
|
|
32
|
+
readonly systemPrompt?: boolean;
|
|
33
|
+
readonly agentPrompt?: boolean;
|
|
34
|
+
readonly repoPrompt?: boolean;
|
|
35
|
+
readonly globalSkills?: boolean;
|
|
36
|
+
readonly agentSkills?: boolean;
|
|
37
|
+
readonly repoSkills?: boolean;
|
|
38
|
+
readonly globalTools?: boolean;
|
|
39
|
+
readonly agentTools?: boolean;
|
|
40
|
+
readonly repoTools?: boolean;
|
|
41
|
+
}
|
|
42
|
+
/** Lightweight envelope describing an app-config agent bundle discovered on disk. */
|
|
43
|
+
export interface AgentBundle {
|
|
44
|
+
/** Agent name (from the containing directory or AGENT.md frontmatter, validated later). */
|
|
45
|
+
readonly name: string;
|
|
46
|
+
/** Absolute path to the per-agent `AGENT.md` file. */
|
|
47
|
+
readonly path: string;
|
|
48
|
+
/** Root directory that was scanned. */
|
|
49
|
+
readonly configRoot: string;
|
|
50
|
+
/** Path to the app-global `SYSTEM.md` prompt, if present. */
|
|
51
|
+
readonly systemPromptPath?: string;
|
|
52
|
+
/** Paths to global skills under `<configRoot>/agents/skills/`. */
|
|
53
|
+
readonly globalSkills: readonly string[];
|
|
54
|
+
/** Paths to global tools under `<configRoot>/agents/tools/`. */
|
|
55
|
+
readonly globalTools: readonly string[];
|
|
56
|
+
/** Paths to agent-specific skills under `<agentDir>/skills/`. */
|
|
57
|
+
readonly agentSkills: readonly string[];
|
|
58
|
+
/** Paths to agent-specific tools under `<agentDir>/tools/`. */
|
|
59
|
+
readonly agentTools: readonly string[];
|
|
60
|
+
}
|
|
61
|
+
/** Options for {@link discoverAgentBundles}. */
|
|
62
|
+
export interface DiscoverAgentBundlesOptions {
|
|
63
|
+
/** App configuration root. Scans `<configRoot>/agents/`. */
|
|
64
|
+
readonly configRoot: string;
|
|
65
|
+
readonly trust?: TrustPolicy;
|
|
66
|
+
readonly permission?: PermissionPolicy;
|
|
67
|
+
readonly signal?: AbortSignal;
|
|
68
|
+
}
|
|
69
|
+
/** Discover app-config agent bundles under `<configRoot>/agents/`.
|
|
70
|
+
*
|
|
71
|
+
* Returns one {@link AgentBundle} per subdirectory containing an `AGENT.md` file.
|
|
72
|
+
* Also collects paths to the app-global `SYSTEM.md`, global skills/tools, and
|
|
73
|
+
* per-agent skills/tools. No file content is parsed here; resolution happens in
|
|
74
|
+
* {@link resolveAgentBundle}. Trust/permission policies apply to `configRoot`
|
|
75
|
+
* and its subdirectories; symlinks escaping `configRoot/agents` are excluded.
|
|
76
|
+
*/
|
|
77
|
+
export declare function discoverAgentBundles(options: DiscoverAgentBundlesOptions): Promise<readonly AgentBundle[]>;
|
|
78
|
+
/** Resolve a discovered {@link AgentBundle} into a runnable {@link Agent}.
|
|
79
|
+
*
|
|
80
|
+
* Builds union tool/skill registries across the included scopes (global,
|
|
81
|
+
* agent-specific, and repo-level). Duplicate names across included scopes throw
|
|
82
|
+
* rather than override. System prompts append in fixed order
|
|
83
|
+
* `SYSTEM.md` → `AGENT.md` → `AGENTS.md` when enabled, before delegating to
|
|
84
|
+
* {@link resolveAgentDefinition}. No `import()` is performed; descriptor-only
|
|
85
|
+
* repo tools throw if executed.
|
|
86
|
+
*
|
|
87
|
+
* The caller controls scope by which registries and flags it passes. Missing
|
|
88
|
+
* dependencies fail closed at resolution time. Each prompt file is read at
|
|
89
|
+
* most once per call; resolution is one-shot, so no cross-call cache is kept
|
|
90
|
+
* (ponytail: add memoization only if a host resolves the same bundle repeatedly). */
|
|
91
|
+
export declare function resolveAgentBundle(bundle: AgentBundle, options: ResolveAgentBundleOptions): Promise<Agent>;
|
|
92
|
+
/** Parse a `CONTEXT.md` into a static {@link ContextProvider}. The markdown
|
|
93
|
+
* body becomes the context block content; frontmatter `name` overrides the
|
|
94
|
+
* directory name. */
|
|
95
|
+
export declare function parseContextFile(text: string, path: string): ContextProvider;
|
|
96
|
+
/** Parse a `TOOL.md` into a descriptor-only {@link ToolDefinition}. The
|
|
97
|
+
* executable behavior is host-owned; calling `execute` throws. */
|
|
98
|
+
export declare function parseToolFile(text: string, path: string): ToolDefinition;
|
|
@@ -0,0 +1,389 @@
|
|
|
1
|
+
import { join } from "node:path";
|
|
2
|
+
import { readFile } from "node:fs/promises";
|
|
3
|
+
import { readdir, stat } from "node:fs/promises";
|
|
4
|
+
import { parseSkillFile, splitFrontmatter } from "../contribution-parsing.js";
|
|
5
|
+
import { resolveAgentDefinition } from "../agent-definitions.js";
|
|
6
|
+
import { assertPermission, isTrusted } from "../security.js";
|
|
7
|
+
import { isPathInsideReal } from "./trust.js";
|
|
8
|
+
import { readOptionalFile } from "./contribution-discovery.js";
|
|
9
|
+
import { createSkillRegistry } from "../skills.js";
|
|
10
|
+
const defaultInclude = {
|
|
11
|
+
systemPrompt: true,
|
|
12
|
+
agentPrompt: true,
|
|
13
|
+
repoPrompt: true,
|
|
14
|
+
globalSkills: true,
|
|
15
|
+
agentSkills: true,
|
|
16
|
+
repoSkills: true,
|
|
17
|
+
globalTools: true,
|
|
18
|
+
agentTools: true,
|
|
19
|
+
repoTools: true,
|
|
20
|
+
};
|
|
21
|
+
/** Discover app-config agent bundles under `<configRoot>/agents/`.
|
|
22
|
+
*
|
|
23
|
+
* Returns one {@link AgentBundle} per subdirectory containing an `AGENT.md` file.
|
|
24
|
+
* Also collects paths to the app-global `SYSTEM.md`, global skills/tools, and
|
|
25
|
+
* per-agent skills/tools. No file content is parsed here; resolution happens in
|
|
26
|
+
* {@link resolveAgentBundle}. Trust/permission policies apply to `configRoot`
|
|
27
|
+
* and its subdirectories; symlinks escaping `configRoot/agents` are excluded.
|
|
28
|
+
*/
|
|
29
|
+
export async function discoverAgentBundles(options) {
|
|
30
|
+
const { configRoot, trust, permission, signal } = options;
|
|
31
|
+
if (signal?.aborted)
|
|
32
|
+
throw signal.reason;
|
|
33
|
+
if (trust) {
|
|
34
|
+
const decision = await trust.check({ kind: "project", target: configRoot });
|
|
35
|
+
if (!decision.trusted)
|
|
36
|
+
return [];
|
|
37
|
+
}
|
|
38
|
+
const agentsDir = join(configRoot, "agents");
|
|
39
|
+
const systemPromptPath = await findSystemPromptPath(agentsDir);
|
|
40
|
+
const globalSkills = await scanSkillPaths(join(agentsDir, "skills"), agentsDir, permission);
|
|
41
|
+
const globalTools = await scanToolPaths(join(agentsDir, "tools"), agentsDir, permission);
|
|
42
|
+
const bundles = [];
|
|
43
|
+
for (const name of await listSubdirs(agentsDir)) {
|
|
44
|
+
if (name === "skills" || name === "tools")
|
|
45
|
+
continue;
|
|
46
|
+
const agentDir = join(agentsDir, name);
|
|
47
|
+
const agentPath = join(agentDir, "AGENT.md");
|
|
48
|
+
if ((await readOptionalFile(agentPath)) === undefined)
|
|
49
|
+
continue;
|
|
50
|
+
await assertPermission(permission, { kind: "resource", action: "load", target: agentDir });
|
|
51
|
+
if (!(await isPathInsideReal(agentsDir, agentDir)))
|
|
52
|
+
continue;
|
|
53
|
+
const agentSkills = await scanSkillPaths(join(agentDir, "skills"), agentsDir, permission);
|
|
54
|
+
const agentTools = await scanToolPaths(join(agentDir, "tools"), agentsDir, permission);
|
|
55
|
+
bundles.push({
|
|
56
|
+
name,
|
|
57
|
+
path: agentPath,
|
|
58
|
+
configRoot,
|
|
59
|
+
systemPromptPath,
|
|
60
|
+
globalSkills,
|
|
61
|
+
globalTools,
|
|
62
|
+
agentSkills,
|
|
63
|
+
agentTools,
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
return bundles;
|
|
67
|
+
}
|
|
68
|
+
async function findSystemPromptPath(agentsDir) {
|
|
69
|
+
const path = join(agentsDir, "SYSTEM.md");
|
|
70
|
+
return (await readOptionalFile(path)) !== undefined ? path : undefined;
|
|
71
|
+
}
|
|
72
|
+
async function scanSkillPaths(dir, agentsDir, permission) {
|
|
73
|
+
const out = [];
|
|
74
|
+
for (const name of await listSubdirs(dir)) {
|
|
75
|
+
const path = join(dir, name, "SKILL.md");
|
|
76
|
+
if ((await readOptionalFile(path)) === undefined)
|
|
77
|
+
continue;
|
|
78
|
+
await assertPermission(permission, { kind: "resource", action: "load", target: join(dir, name) });
|
|
79
|
+
if (!(await isPathInsideReal(agentsDir, path)))
|
|
80
|
+
continue;
|
|
81
|
+
out.push(path);
|
|
82
|
+
}
|
|
83
|
+
return out;
|
|
84
|
+
}
|
|
85
|
+
async function scanToolPaths(dir, agentsDir, permission) {
|
|
86
|
+
const out = [];
|
|
87
|
+
for (const name of await listSubdirs(dir)) {
|
|
88
|
+
const path = join(dir, name, "manifest.json");
|
|
89
|
+
if ((await readOptionalFile(path)) === undefined)
|
|
90
|
+
continue;
|
|
91
|
+
await assertPermission(permission, { kind: "resource", action: "load", target: join(dir, name) });
|
|
92
|
+
if (!(await isPathInsideReal(agentsDir, path)))
|
|
93
|
+
continue;
|
|
94
|
+
out.push(path);
|
|
95
|
+
}
|
|
96
|
+
return out;
|
|
97
|
+
}
|
|
98
|
+
/** Resolve a discovered {@link AgentBundle} into a runnable {@link Agent}.
|
|
99
|
+
*
|
|
100
|
+
* Builds union tool/skill registries across the included scopes (global,
|
|
101
|
+
* agent-specific, and repo-level). Duplicate names across included scopes throw
|
|
102
|
+
* rather than override. System prompts append in fixed order
|
|
103
|
+
* `SYSTEM.md` → `AGENT.md` → `AGENTS.md` when enabled, before delegating to
|
|
104
|
+
* {@link resolveAgentDefinition}. No `import()` is performed; descriptor-only
|
|
105
|
+
* repo tools throw if executed.
|
|
106
|
+
*
|
|
107
|
+
* The caller controls scope by which registries and flags it passes. Missing
|
|
108
|
+
* dependencies fail closed at resolution time. Each prompt file is read at
|
|
109
|
+
* most once per call; resolution is one-shot, so no cross-call cache is kept
|
|
110
|
+
* (ponytail: add memoization only if a host resolves the same bundle repeatedly). */
|
|
111
|
+
export async function resolveAgentBundle(bundle, options) {
|
|
112
|
+
const read = options.readFile ?? ((path) => readFile(path, "utf8"));
|
|
113
|
+
const text = await read(bundle.path);
|
|
114
|
+
const { front } = splitFrontmatter(text, bundle.path);
|
|
115
|
+
const include = { ...defaultInclude, ...(options.include ?? {}) };
|
|
116
|
+
const skillSources = await buildSkillSources(bundle, options, read, include);
|
|
117
|
+
const skills = unionSkills(skillSources);
|
|
118
|
+
const toolSources = buildToolSources(bundle, options, include);
|
|
119
|
+
const tools = unionTools(toolSources);
|
|
120
|
+
const systemPrompts = await buildSystemPrompts(bundle, options, read, text, include);
|
|
121
|
+
const rawName = String(front.get("name") ?? "").trim() || parentDirName(bundle.path);
|
|
122
|
+
if (!NAME_RE.test(rawName)) {
|
|
123
|
+
throw new Error(`Invalid agent name in ${bundle.path}: ${JSON.stringify(rawName)}`);
|
|
124
|
+
}
|
|
125
|
+
const def = {
|
|
126
|
+
name: rawName.replace(/\s+/g, "-"),
|
|
127
|
+
description: getString(front, "description"),
|
|
128
|
+
model: getString(front, "model"),
|
|
129
|
+
tools: getStringList(front, "tools"),
|
|
130
|
+
skills: getStringList(front, "skills"),
|
|
131
|
+
context: getStringList(front, "context"),
|
|
132
|
+
instructions: getString(front, "instructions"),
|
|
133
|
+
...(systemPrompts.length > 0 ? { systemPrompt: systemPrompts } : {}),
|
|
134
|
+
metadata: collectMetadata(front),
|
|
135
|
+
};
|
|
136
|
+
// ponytail: SKILL.md context names are resolved against the host context-provider registry.
|
|
137
|
+
resolveSkillContexts(skills, options.registries?.contextProviders);
|
|
138
|
+
return resolveAgentDefinition(def, {
|
|
139
|
+
registries: options.registries,
|
|
140
|
+
providerSource: options.providerSource,
|
|
141
|
+
tools: tools.length > 0 ? tools : options.tools,
|
|
142
|
+
skillsRegistry: skills.length > 0 ? createSkillRegistry(skills) : options.skillsRegistry,
|
|
143
|
+
activateAllCapabilities: options.activateAllCapabilities,
|
|
144
|
+
overrides: options.overrides,
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
async function buildSkillSources(bundle, options, read, include) {
|
|
148
|
+
const out = [];
|
|
149
|
+
if (include.globalSkills) {
|
|
150
|
+
out.push({ scope: "global", skills: await parseSkillPaths(bundle.globalSkills, read) });
|
|
151
|
+
}
|
|
152
|
+
if (include.agentSkills) {
|
|
153
|
+
out.push({ scope: "agent", skills: await parseSkillPaths(bundle.agentSkills, read) });
|
|
154
|
+
}
|
|
155
|
+
if (include.repoSkills && options.repoContributions) {
|
|
156
|
+
const skills = options.repoContributions
|
|
157
|
+
.filter((c) => c.kind === "skill" && c.skill !== undefined)
|
|
158
|
+
.map((c) => c.skill);
|
|
159
|
+
out.push({ scope: "repo", skills });
|
|
160
|
+
}
|
|
161
|
+
return out;
|
|
162
|
+
}
|
|
163
|
+
function buildToolSources(bundle, options, include) {
|
|
164
|
+
const out = [];
|
|
165
|
+
if (include.globalTools) {
|
|
166
|
+
out.push({ scope: "global", tools: bundle.globalTools.map((path) => toolDescriptorFromPath(path)) });
|
|
167
|
+
}
|
|
168
|
+
if (include.agentTools) {
|
|
169
|
+
out.push({ scope: "agent", tools: bundle.agentTools.map((path) => toolDescriptorFromPath(path)) });
|
|
170
|
+
}
|
|
171
|
+
if (include.repoTools && options.repoContributions) {
|
|
172
|
+
const tools = options.repoContributions
|
|
173
|
+
.filter((c) => c.kind === "tool" && c.declaration !== undefined)
|
|
174
|
+
.map((c) => toolDescriptorFromDeclaration(c.declaration));
|
|
175
|
+
out.push({ scope: "repo", tools });
|
|
176
|
+
}
|
|
177
|
+
return out;
|
|
178
|
+
}
|
|
179
|
+
async function buildSystemPrompts(bundle, options, read, agentText, include) {
|
|
180
|
+
const out = [];
|
|
181
|
+
if (include.systemPrompt && bundle.systemPromptPath) {
|
|
182
|
+
// ponytail: trust-gate the app-config root independently from the workspace root. SYSTEM.md is
|
|
183
|
+
// app-supplied, so an untrusted configRoot contributes nothing (fail-closed, no throw).
|
|
184
|
+
if (await isTrusted(options.trust, { kind: "project", target: bundle.systemPromptPath })) {
|
|
185
|
+
await assertPermission(options.permission, { kind: "resource", action: "load", target: bundle.systemPromptPath });
|
|
186
|
+
const text = await readOptional(read, bundle.systemPromptPath);
|
|
187
|
+
if (text !== undefined) {
|
|
188
|
+
out.push({
|
|
189
|
+
id: "agent-system",
|
|
190
|
+
source: "user",
|
|
191
|
+
mode: "append",
|
|
192
|
+
text,
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
if (include.agentPrompt) {
|
|
198
|
+
const text = bodyAsInstructions(agentText, bundle.path);
|
|
199
|
+
if (text) {
|
|
200
|
+
out.push({
|
|
201
|
+
id: "agent-prompt",
|
|
202
|
+
source: "package",
|
|
203
|
+
mode: "append",
|
|
204
|
+
text,
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
if (include.repoPrompt && options.workspaceRoot) {
|
|
209
|
+
const repoPath = join(options.workspaceRoot, "AGENTS.md");
|
|
210
|
+
// ponytail: trust-gate the workspace root independently; untrusted AGENTS.md is skipped silently.
|
|
211
|
+
if (await isTrusted(options.trust, { kind: "project", target: repoPath })) {
|
|
212
|
+
await assertPermission(options.permission, { kind: "resource", action: "load", target: repoPath });
|
|
213
|
+
const text = await readOptional(read, repoPath);
|
|
214
|
+
if (text !== undefined) {
|
|
215
|
+
out.push({
|
|
216
|
+
id: "repo-project-prompt",
|
|
217
|
+
source: "app",
|
|
218
|
+
mode: "append",
|
|
219
|
+
text,
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
return out;
|
|
225
|
+
}
|
|
226
|
+
async function parseSkillPaths(paths, read) {
|
|
227
|
+
const out = [];
|
|
228
|
+
for (const path of paths) {
|
|
229
|
+
const text = await read(path);
|
|
230
|
+
out.push(parseSkillFile(text, path));
|
|
231
|
+
}
|
|
232
|
+
return out;
|
|
233
|
+
}
|
|
234
|
+
function unionSkills(sources) {
|
|
235
|
+
const seen = new Map();
|
|
236
|
+
const out = [];
|
|
237
|
+
for (const { scope, skills } of sources) {
|
|
238
|
+
for (const skill of skills) {
|
|
239
|
+
if (seen.has(skill.name)) {
|
|
240
|
+
throw new Error(`Duplicate skill name across scopes: ${skill.name} (found in ${scope} and ${seen.get(skill.name)})`);
|
|
241
|
+
}
|
|
242
|
+
seen.set(skill.name, scope);
|
|
243
|
+
out.push(skill);
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
return out;
|
|
247
|
+
}
|
|
248
|
+
function unionTools(sources) {
|
|
249
|
+
const seen = new Map();
|
|
250
|
+
const out = [];
|
|
251
|
+
for (const { scope, tools } of sources) {
|
|
252
|
+
for (const tool of tools) {
|
|
253
|
+
if (seen.has(tool.name)) {
|
|
254
|
+
throw new Error(`Duplicate tool name across scopes: ${tool.name} (found in ${scope} and ${seen.get(tool.name)})`);
|
|
255
|
+
}
|
|
256
|
+
seen.set(tool.name, scope);
|
|
257
|
+
out.push(tool);
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
return out;
|
|
261
|
+
}
|
|
262
|
+
function toolDescriptorFromPath(path) {
|
|
263
|
+
const name = parentDirName(path);
|
|
264
|
+
return toolDescriptor(name);
|
|
265
|
+
}
|
|
266
|
+
function toolDescriptorFromDeclaration(declaration) {
|
|
267
|
+
return toolDescriptor(declaration.name);
|
|
268
|
+
}
|
|
269
|
+
function toolDescriptor(name) {
|
|
270
|
+
return {
|
|
271
|
+
name,
|
|
272
|
+
execute: () => {
|
|
273
|
+
throw new Error(`Discovered tool ${name} requires host execution`);
|
|
274
|
+
},
|
|
275
|
+
};
|
|
276
|
+
}
|
|
277
|
+
function resolveSkillContexts(skills, contextProviders) {
|
|
278
|
+
if (!contextProviders)
|
|
279
|
+
return;
|
|
280
|
+
for (const skill of skills) {
|
|
281
|
+
const names = skill.metadata?.context;
|
|
282
|
+
if (!Array.isArray(names))
|
|
283
|
+
continue;
|
|
284
|
+
skill.context = names.map((name) => contextProviders.resolve(String(name)));
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
function collectMetadata(front) {
|
|
288
|
+
const metadata = {};
|
|
289
|
+
for (const [key, value] of front) {
|
|
290
|
+
if (key !== "name" && key !== "description" && key !== "model" && key !== "tools" && key !== "skills" && key !== "context" && key !== "instructions") {
|
|
291
|
+
metadata[key] = value;
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
return Object.keys(metadata).length > 0 ? metadata : undefined;
|
|
295
|
+
}
|
|
296
|
+
function getString(front, key) {
|
|
297
|
+
const value = front.get(key);
|
|
298
|
+
if (typeof value === "string")
|
|
299
|
+
return value;
|
|
300
|
+
return undefined;
|
|
301
|
+
}
|
|
302
|
+
function getStringList(front, key) {
|
|
303
|
+
const value = front.get(key);
|
|
304
|
+
if (Array.isArray(value))
|
|
305
|
+
return value.map(String);
|
|
306
|
+
if (typeof value === "string")
|
|
307
|
+
return value.split(",").map((s) => s.trim()).filter(Boolean);
|
|
308
|
+
return undefined;
|
|
309
|
+
}
|
|
310
|
+
function bodyAsInstructions(text, path) {
|
|
311
|
+
const { body } = splitFrontmatter(text, path);
|
|
312
|
+
const trimmed = body.replace(/^\n+/, "").trimEnd();
|
|
313
|
+
return trimmed.length > 0 ? trimmed : undefined;
|
|
314
|
+
}
|
|
315
|
+
async function listSubdirs(dir) {
|
|
316
|
+
try {
|
|
317
|
+
const entries = await readdir(dir);
|
|
318
|
+
const out = [];
|
|
319
|
+
for (const entry of entries) {
|
|
320
|
+
const path = join(dir, entry);
|
|
321
|
+
try {
|
|
322
|
+
if ((await stat(path)).isDirectory())
|
|
323
|
+
out.push(entry);
|
|
324
|
+
}
|
|
325
|
+
catch {
|
|
326
|
+
// ignore stat errors for individual entries
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
return out;
|
|
330
|
+
}
|
|
331
|
+
catch (error) {
|
|
332
|
+
if (isMissingFile(error))
|
|
333
|
+
return [];
|
|
334
|
+
throw error;
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
async function readOptional(read, path) {
|
|
338
|
+
try {
|
|
339
|
+
return await read(path);
|
|
340
|
+
}
|
|
341
|
+
catch (error) {
|
|
342
|
+
if (isMissingFile(error))
|
|
343
|
+
return undefined;
|
|
344
|
+
throw error;
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
function isMissingFile(error) {
|
|
348
|
+
return error instanceof Error && "code" in error && error.code === "ENOENT";
|
|
349
|
+
}
|
|
350
|
+
const NAME_RE = /^[A-Za-z0-9 _-]+$/;
|
|
351
|
+
function parentDirName(path) {
|
|
352
|
+
const parts = path.split(/[/\\]+/).filter(Boolean);
|
|
353
|
+
return parts.length >= 2 ? parts[parts.length - 2] : parts[parts.length - 1] ?? "";
|
|
354
|
+
}
|
|
355
|
+
/** Parse a `CONTEXT.md` into a static {@link ContextProvider}. The markdown
|
|
356
|
+
* body becomes the context block content; frontmatter `name` overrides the
|
|
357
|
+
* directory name. */
|
|
358
|
+
export function parseContextFile(text, path) {
|
|
359
|
+
const { front, body } = splitFrontmatter(text, path);
|
|
360
|
+
const rawName = String(front.get("name") ?? "").trim() || parentDirName(path);
|
|
361
|
+
if (!NAME_RE.test(rawName)) {
|
|
362
|
+
throw new Error(`Invalid context provider name in ${path}: ${JSON.stringify(rawName)}`);
|
|
363
|
+
}
|
|
364
|
+
const name = rawName.replace(/\s+/g, "-");
|
|
365
|
+
const title = String(front.get("title") ?? name);
|
|
366
|
+
return {
|
|
367
|
+
name,
|
|
368
|
+
resolve: () => [{ title, content: body.replace(/^\n+/, "").trimEnd() }],
|
|
369
|
+
};
|
|
370
|
+
}
|
|
371
|
+
/** Parse a `TOOL.md` into a descriptor-only {@link ToolDefinition}. The
|
|
372
|
+
* executable behavior is host-owned; calling `execute` throws. */
|
|
373
|
+
export function parseToolFile(text, path) {
|
|
374
|
+
const { front, body } = splitFrontmatter(text, path);
|
|
375
|
+
const rawName = String(front.get("name") ?? "").trim() || parentDirName(path);
|
|
376
|
+
if (!NAME_RE.test(rawName)) {
|
|
377
|
+
throw new Error(`Invalid tool name in ${path}: ${JSON.stringify(rawName)}`);
|
|
378
|
+
}
|
|
379
|
+
const name = rawName.replace(/\s+/g, "-");
|
|
380
|
+
const description = String(front.get("description") ?? "").trim() || body.replace(/^\n+/, "").slice(0, 80).trim();
|
|
381
|
+
return {
|
|
382
|
+
name,
|
|
383
|
+
...(description ? { description } : {}),
|
|
384
|
+
execute: () => {
|
|
385
|
+
throw new Error(`Discovered tool ${name} requires host execution`);
|
|
386
|
+
},
|
|
387
|
+
};
|
|
388
|
+
}
|
|
389
|
+
//# sourceMappingURL=agent-definitions.js.map
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { ContributionFileKind, DiscoveredContribution } from "../contracts.js";
|
|
2
|
+
import type { PermissionPolicy } from "../security.js";
|
|
3
|
+
import type { TrustPolicy } from "../security.js";
|
|
4
|
+
export interface DiscoveryOptions {
|
|
5
|
+
readonly kinds: readonly ContributionFileKind[];
|
|
6
|
+
/** Workspace root. Scans `<root>/.agents/<kind>s/<name>/`. Gated by `trust`. */
|
|
7
|
+
readonly workspaceRoot?: string;
|
|
8
|
+
readonly permission?: PermissionPolicy;
|
|
9
|
+
readonly trust?: TrustPolicy;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Discover contributions on disk. Scans the workspace `.agents/` tree only;
|
|
13
|
+
* no global root. One `readdir` per kind-root; no `import()`. Inert output —
|
|
14
|
+
* executable behavior is host-owned.
|
|
15
|
+
*/
|
|
16
|
+
export declare function discoverContributions(options: DiscoveryOptions): Promise<readonly DiscoveredContribution[]>;
|
|
17
|
+
export declare function readOptionalFile(path: string): Promise<string | undefined>;
|