@arnilo/prism 0.0.1 → 0.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.
Files changed (120) hide show
  1. package/CHANGELOG.md +4 -2
  2. package/README.md +17 -7
  3. package/dist/agent-definitions.d.ts +12 -0
  4. package/dist/agent-definitions.js +131 -0
  5. package/dist/agent-loops.d.ts +14 -0
  6. package/dist/agent-loops.js +161 -0
  7. package/dist/agents.js +263 -76
  8. package/dist/cache-helpers.d.ts +28 -0
  9. package/dist/cache-helpers.js +73 -0
  10. package/dist/cli-runner.d.ts +38 -2
  11. package/dist/cli-runner.js +167 -5
  12. package/dist/compaction.js +2 -0
  13. package/dist/config.js +47 -12
  14. package/dist/contracts.d.ts +581 -6
  15. package/dist/contracts.js +41 -1
  16. package/dist/contribution-parsing.d.ts +19 -0
  17. package/dist/contribution-parsing.js +124 -0
  18. package/dist/contributions.d.ts +13 -3
  19. package/dist/contributions.js +96 -20
  20. package/dist/extensions.js +3 -0
  21. package/dist/index.d.ts +19 -9
  22. package/dist/index.js +10 -4
  23. package/dist/input.d.ts +7 -1
  24. package/dist/input.js +52 -11
  25. package/dist/instruction-injection.d.ts +28 -0
  26. package/dist/instruction-injection.js +55 -0
  27. package/dist/manifests.d.ts +1 -1
  28. package/dist/manifests.js +3 -3
  29. package/dist/models.d.ts +4 -1
  30. package/dist/models.js +5 -2
  31. package/dist/node/agent-definitions.d.ts +98 -0
  32. package/dist/node/agent-definitions.js +389 -0
  33. package/dist/node/contribution-discovery.d.ts +17 -0
  34. package/dist/node/contribution-discovery.js +163 -0
  35. package/dist/node/instruction-injectors.d.ts +32 -0
  36. package/dist/node/instruction-injectors.js +72 -0
  37. package/dist/node/session-store-jsonl.d.ts +1 -1
  38. package/dist/node/session-store-jsonl.js +42 -4
  39. package/dist/node/system-project-prompts.d.ts +30 -0
  40. package/dist/node/system-project-prompts.js +53 -0
  41. package/dist/provider-events.d.ts +3 -1
  42. package/dist/provider-events.js +34 -0
  43. package/dist/provider-request-policy.js +15 -1
  44. package/dist/providers/openai-compatible.js +1 -1
  45. package/dist/providers.d.ts +6 -2
  46. package/dist/providers.js +15 -1
  47. package/dist/redaction.d.ts +2 -1
  48. package/dist/redaction.js +3 -0
  49. package/dist/registry-options.d.ts +5 -0
  50. package/dist/registry-options.js +5 -0
  51. package/dist/rpc.d.ts +6 -2
  52. package/dist/rpc.js +71 -13
  53. package/dist/session-stores.d.ts +3 -1
  54. package/dist/session-stores.js +67 -6
  55. package/dist/skills.d.ts +4 -1
  56. package/dist/skills.js +3 -1
  57. package/dist/system-prompts.js +6 -2
  58. package/dist/testing/compaction-conformance.d.ts +17 -0
  59. package/dist/testing/compaction-conformance.js +61 -0
  60. package/dist/testing/extension-conformance.d.ts +26 -0
  61. package/dist/testing/extension-conformance.js +55 -0
  62. package/dist/testing/provider-conformance.d.ts +7 -0
  63. package/dist/testing/provider-conformance.js +18 -31
  64. package/dist/testing/session-store-conformance.d.ts +20 -0
  65. package/dist/testing/session-store-conformance.js +92 -0
  66. package/dist/testing/tool-conformance.d.ts +39 -0
  67. package/dist/testing/tool-conformance.js +79 -0
  68. package/dist/tools.d.ts +7 -2
  69. package/dist/tools.js +50 -13
  70. package/docs/agent-definitions.md +251 -0
  71. package/docs/agent-events.md +199 -0
  72. package/docs/agent-loops.md +217 -0
  73. package/docs/agent-session-runtime.md +20 -8
  74. package/docs/cli-rpc.md +39 -4
  75. package/docs/compaction-and-retry.md +2 -2
  76. package/docs/compaction-conformance.md +76 -0
  77. package/docs/compaction-llm.md +6 -3
  78. package/docs/compaction-observational-memory.md +4 -4
  79. package/docs/configuration-and-manifests.md +6 -1
  80. package/docs/context-and-skills.md +79 -6
  81. package/docs/contribution-discovery.md +149 -0
  82. package/docs/contribution-registries.md +9 -6
  83. package/docs/credentials-and-redaction.md +2 -0
  84. package/docs/customization.md +191 -0
  85. package/docs/database-persistence.md +407 -0
  86. package/docs/extension-authoring.md +193 -0
  87. package/docs/extension-conformance.md +80 -0
  88. package/docs/extensions.md +6 -0
  89. package/docs/host-security.md +141 -0
  90. package/docs/index.md +40 -19
  91. package/docs/input-and-prompt-assembly.md +19 -3
  92. package/docs/instruction-injection.md +183 -0
  93. package/docs/migration.md +201 -0
  94. package/docs/model-registry.md +122 -0
  95. package/docs/node-jsonl-session-store.md +5 -4
  96. package/docs/performance.md +127 -0
  97. package/docs/provider-caching.md +206 -0
  98. package/docs/provider-conformance.md +32 -5
  99. package/docs/provider-layer.md +51 -11
  100. package/docs/provider-packages.md +65 -5
  101. package/docs/provider-request-policies.md +113 -0
  102. package/docs/providers/kimi.md +22 -0
  103. package/docs/providers/neuralwatt.md +388 -0
  104. package/docs/providers/openai-compatible.md +1 -0
  105. package/docs/providers/openai.md +21 -0
  106. package/docs/providers/opencode-go.md +31 -3
  107. package/docs/providers/openrouter.md +29 -0
  108. package/docs/providers/zai.md +17 -0
  109. package/docs/public-contracts.md +87 -12
  110. package/docs/release-and-install.md +76 -26
  111. package/docs/runs-and-usage.md +236 -0
  112. package/docs/session-store-conformance.md +78 -0
  113. package/docs/session-stores-and-branching.md +10 -6
  114. package/docs/session-stores.md +126 -0
  115. package/docs/settings-auth-trust-security.md +18 -4
  116. package/docs/structured-output.md +247 -0
  117. package/docs/system-prompts.md +104 -2
  118. package/docs/tool-conformance.md +87 -0
  119. package/docs/tools.md +64 -8
  120. package/package.json +35 -2
@@ -0,0 +1,163 @@
1
+ import { readdir, readFile, stat } from "node:fs/promises";
2
+ import { join } from "node:path";
3
+ import { isJsonObject } from "../config.js";
4
+ import { assertPermission } from "../security.js";
5
+ import { isPathInsideReal } from "./trust.js";
6
+ import { parseSkillFile } from "../contribution-parsing.js";
7
+ /**
8
+ * Discover contributions on disk. Scans the workspace `.agents/` tree only;
9
+ * no global root. One `readdir` per kind-root; no `import()`. Inert output —
10
+ * executable behavior is host-owned.
11
+ */
12
+ export async function discoverContributions(options) {
13
+ const kinds = options.kinds;
14
+ const merged = new Map();
15
+ if (options.workspaceRoot) {
16
+ for (const kind of kinds) {
17
+ const entries = await scanKindRoot(options.workspaceRoot, kind, options);
18
+ for (const c of entries)
19
+ merged.set(`${c.kind}/${c.name}`, c);
20
+ }
21
+ }
22
+ return [...merged.values()];
23
+ }
24
+ // ponytail: single-level named-subdir scan; nested layouts would need a recursive walk if added later.
25
+ async function scanKindRoot(root, kind, options) {
26
+ const kindDir = join(root, ".agents", kindDirName(kind));
27
+ if (options.trust) {
28
+ const decision = await options.trust.check({ kind: "project", target: kindDir });
29
+ if (!decision.trusted)
30
+ return []; // untrusted workspace root: skip, no throw
31
+ }
32
+ let names;
33
+ try {
34
+ names = await readdir(kindDir);
35
+ }
36
+ catch (error) {
37
+ if (isMissingFile(error))
38
+ return []; // missing kind root is normal
39
+ throw error;
40
+ }
41
+ const out = [];
42
+ for (const name of names) {
43
+ const dir = join(kindDir, name);
44
+ let isDir;
45
+ try {
46
+ isDir = (await stat(dir)).isDirectory();
47
+ }
48
+ catch (error) {
49
+ if (isMissingFile(error))
50
+ continue;
51
+ throw error;
52
+ }
53
+ if (!isDir)
54
+ continue;
55
+ // Symlinks escaping the kind root are excluded via realpath containment.
56
+ if (!(await isPathInsideReal(kindDir, dir)))
57
+ continue;
58
+ await assertPermission(options.permission, { kind: "resource", action: "load", target: dir });
59
+ const entry = await readEntry(dir, name, kind, "workspace");
60
+ if (entry)
61
+ out.push(entry);
62
+ }
63
+ return out;
64
+ }
65
+ async function readEntry(dir, fallbackName, kind, origin) {
66
+ switch (kind) {
67
+ case "skill":
68
+ return readSkillEntry(dir, fallbackName, origin);
69
+ default:
70
+ // tool / context / instructions → manifest.json declaration
71
+ return readManifestEntry(dir, fallbackName, kind, origin);
72
+ }
73
+ }
74
+ async function readSkillEntry(dir, fallbackName, origin) {
75
+ const path = join(dir, "SKILL.md");
76
+ if (!(await isPathInsideReal(dir, path)))
77
+ return undefined;
78
+ const text = await readOptionalFile(path);
79
+ if (text === undefined)
80
+ return undefined;
81
+ const skill = parseSkillFile(text, path);
82
+ return { kind: "skill", name: skill.name, origin, path, skill };
83
+ }
84
+ async function readManifestEntry(dir, fallbackName, kind, origin) {
85
+ const path = join(dir, "manifest.json");
86
+ if (!(await isPathInsideReal(dir, path)))
87
+ return undefined;
88
+ const text = await readOptionalFile(path);
89
+ if (text === undefined)
90
+ return undefined;
91
+ const declaration = parseManifestDeclaration(text, path, kind, fallbackName);
92
+ return { kind, name: declaration.name, origin, path, declaration };
93
+ }
94
+ // ponytail: Phase 31 — exported so the system/project prompt file loader reuses the scanner's
95
+ // ENOENT-tolerant read instead of duplicating it. isMissingFile stays private (only readOptionalFile needs it).
96
+ export async function readOptionalFile(path) {
97
+ try {
98
+ return await readFile(path, "utf8");
99
+ }
100
+ catch (error) {
101
+ if (isMissingFile(error))
102
+ return undefined;
103
+ throw error;
104
+ }
105
+ }
106
+ // --- Internal helpers. ---
107
+ // ponytail: parseSkillFile/parseAgentFile live in ../contribution-parsing.ts (core, fs-free); this
108
+ // Node module owns directory walking + manifest.json parsing only.
109
+ function isMissingFile(error) {
110
+ return error instanceof Error && "code" in error && error.code === "ENOENT";
111
+ }
112
+ function parseManifestDeclaration(text, path, dirKind, fallbackName) {
113
+ let parsed;
114
+ try {
115
+ parsed = JSON.parse(text);
116
+ }
117
+ catch (error) {
118
+ throw new Error(`Invalid JSON manifest ${path}: ${error instanceof Error ? error.message : String(error)}`);
119
+ }
120
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
121
+ throw new Error(`Manifest ${path} must be a JSON object`);
122
+ }
123
+ const obj = parsed;
124
+ const name = typeof obj.name === "string" && obj.name.length > 0 ? obj.name : fallbackName;
125
+ const declaration = {
126
+ kind: kindToManifestKind(dirKind),
127
+ name,
128
+ ...(typeof obj.module === "string" ? { module: obj.module } : {}),
129
+ ...(typeof obj.exportName === "string" ? { exportName: obj.exportName } : {}),
130
+ ...(typeof obj.resource === "string" ? { resource: obj.resource } : {}),
131
+ ...(typeof obj.metadata === "object" && obj.metadata !== null && !Array.isArray(obj.metadata) && isJsonObject(obj.metadata)
132
+ ? { metadata: obj.metadata }
133
+ : {}),
134
+ };
135
+ return declaration;
136
+ }
137
+ function kindToManifestKind(kind) {
138
+ switch (kind) {
139
+ case "tool":
140
+ return "tool";
141
+ case "context":
142
+ return "contextProvider";
143
+ case "instructions":
144
+ return "systemPromptContribution";
145
+ case "skill":
146
+ return "skill";
147
+ }
148
+ }
149
+ // ponytail: Phase 29 fix — `${kind}s` produced `instructionss`/`contexts`; the documented layout is
150
+ // `.agents/{skills,tools,context,instructions}/` (instructions and context are already the right form).
151
+ function kindDirName(kind) {
152
+ switch (kind) {
153
+ case "skill":
154
+ return "skills";
155
+ case "tool":
156
+ return "tools";
157
+ case "context":
158
+ return "context";
159
+ case "instructions":
160
+ return "instructions";
161
+ }
162
+ }
163
+ //# sourceMappingURL=contribution-discovery.js.map
@@ -0,0 +1,32 @@
1
+ import type { DiscoveredContribution, InstructionInjector } from "../contracts.js";
2
+ import type { ContributionRegistries } from "../contributions.js";
3
+ import type { PermissionPolicy, TrustPolicy } from "../security.js";
4
+ export interface LoadInstructionInjectorOptions {
5
+ /** Host-owned module loader for declarations with a `module` field. Core never
6
+ * auto-`import()`s (matches Phase 29's tools/context stance); when a module
7
+ * injector is discovered but no loader is supplied, it is skipped. */
8
+ readonly moduleLoader?: (module: string, exportName?: string) => Promise<InstructionInjector>;
9
+ /** Optional escape hatch for instruction resources outside the contribution dir. */
10
+ readonly resourceTrust?: TrustPolicy;
11
+ readonly permission?: PermissionPolicy;
12
+ }
13
+ /**
14
+ * Turn a discovered `kind: "instructions"` contribution into a live
15
+ * {@link InstructionInjector}. Markdown-only contributions (no `module`)
16
+ * load as a static `{ instructions, when: "every_turn" }` injector; module
17
+ * contributions load through the host-supplied `moduleLoader` (or skipped when
18
+ * absent). Core performs no `import()`; fs reads happen only via this Node
19
+ * adapter, never on plain SDK in-memory use.
20
+ *
21
+ * ponytail: reuses Phase 29's scanner output (DiscoveredContribution); this is
22
+ * only the per-kind adapter, mirroring how skills are realized in core. */
23
+ export declare function loadInstructionInjector(contribution: DiscoveredContribution, options?: LoadInstructionInjectorOptions): Promise<InstructionInjector | undefined>;
24
+ /** Load every discovered `instructions` contribution into injectors.
25
+ * Same-name merge order (workspace over global) is already resolved by the
26
+ * Phase 29 scanner (`discoverContributions` dedupes on `(kind, name)`); this
27
+ * adapter only turns the deduped list into injectors. */
28
+ export declare function loadInstructionInjectors(contributions: readonly DiscoveredContribution[], options?: LoadInstructionInjectorOptions): Promise<readonly InstructionInjector[]>;
29
+ /** Load discovered instruction injectors and register them into
30
+ * `registries.instructionInjectors`. Host-owned entry point — never invoked by
31
+ * plain SDK in-memory use (which leaves the registry empty). */
32
+ export declare function registerDiscoveredInstructionInjectors(registries: ContributionRegistries, contributions: readonly DiscoveredContribution[], options?: LoadInstructionInjectorOptions): Promise<void>;
@@ -0,0 +1,72 @@
1
+ import { readFile } from "node:fs/promises";
2
+ import { dirname, isAbsolute, join } from "node:path";
3
+ import { assertPermission, assertTrusted } from "../security.js";
4
+ import { isPathInsideReal } from "./trust.js";
5
+ /**
6
+ * Turn a discovered `kind: "instructions"` contribution into a live
7
+ * {@link InstructionInjector}. Markdown-only contributions (no `module`)
8
+ * load as a static `{ instructions, when: "every_turn" }` injector; module
9
+ * contributions load through the host-supplied `moduleLoader` (or skipped when
10
+ * absent). Core performs no `import()`; fs reads happen only via this Node
11
+ * adapter, never on plain SDK in-memory use.
12
+ *
13
+ * ponytail: reuses Phase 29's scanner output (DiscoveredContribution); this is
14
+ * only the per-kind adapter, mirroring how skills are realized in core. */
15
+ export async function loadInstructionInjector(contribution, options = {}) {
16
+ if (contribution.kind !== "instructions")
17
+ return undefined;
18
+ const name = contribution.name;
19
+ const decl = contribution.declaration;
20
+ // ponytail: module-referenced injector — host owns import(); no core auto-import.
21
+ if (decl?.module) {
22
+ if (!options.moduleLoader)
23
+ return undefined;
24
+ const loaded = await options.moduleLoader(decl.module, decl.exportName);
25
+ return { ...loaded, name };
26
+ }
27
+ // markdown-only instructions: read resource text → static every_turn injector.
28
+ const text = await readInstructionsText(contribution, options);
29
+ return {
30
+ name,
31
+ description: `Discovered instruction injector ${name}`,
32
+ apply: () => ({ instructions: text, when: "every_turn" }),
33
+ };
34
+ }
35
+ /** Load every discovered `instructions` contribution into injectors.
36
+ * Same-name merge order (workspace over global) is already resolved by the
37
+ * Phase 29 scanner (`discoverContributions` dedupes on `(kind, name)`); this
38
+ * adapter only turns the deduped list into injectors. */
39
+ export async function loadInstructionInjectors(contributions, options = {}) {
40
+ const injectors = [];
41
+ for (const contribution of contributions) {
42
+ if (contribution.kind !== "instructions")
43
+ continue;
44
+ const injector = await loadInstructionInjector(contribution, options);
45
+ if (injector)
46
+ injectors.push(injector);
47
+ }
48
+ return injectors;
49
+ }
50
+ /** Load discovered instruction injectors and register them into
51
+ * `registries.instructionInjectors`. Host-owned entry point — never invoked by
52
+ * plain SDK in-memory use (which leaves the registry empty). */
53
+ export async function registerDiscoveredInstructionInjectors(registries, contributions, options = {}) {
54
+ const injectors = await loadInstructionInjectors(contributions, options);
55
+ const registry = registries.instructionInjectors;
56
+ for (const injector of injectors)
57
+ registry.register(injector.name, injector);
58
+ }
59
+ async function readInstructionsText(contribution, options) {
60
+ const dir = dirname(contribution.path);
61
+ const resource = contribution.declaration?.resource;
62
+ // ponytail: resource resolved against the manifest dir; outside reads need explicit host trust.
63
+ const target = resource ? (isAbsolute(resource) ? resource : join(dir, resource)) : join(dir, "INSTRUCTIONS.md");
64
+ if (!(await isPathInsideReal(dir, target))) {
65
+ if (!options.resourceTrust)
66
+ throw new Error(`Instruction resource escapes contribution directory: ${target}`);
67
+ await assertTrusted(options.resourceTrust, { kind: "resource", target, capability: "instruction-resource" });
68
+ }
69
+ await assertPermission(options.permission, { kind: "resource", action: "load", target });
70
+ return await readFile(target, "utf8");
71
+ }
72
+ //# sourceMappingURL=instruction-injectors.js.map
@@ -1,4 +1,4 @@
1
- import type { SessionEntry, SessionStore } from "../contracts.js";
1
+ import { type SessionEntry, type SessionStore } from "../contracts.js";
2
2
  export interface JsonlSessionStoreOptions {
3
3
  readonly path: string;
4
4
  readonly createDirectory?: boolean;
@@ -1,16 +1,35 @@
1
1
  import { mkdir, readFile, appendFile } from "node:fs/promises";
2
2
  import { dirname } from "node:path";
3
+ import { SESSION_APPEND_CONFLICT_CODE, SessionAppendConflictError, isSessionEntryKind, SESSION_ENTRY_SCHEMA_VERSION } from "../contracts.js";
3
4
  export function createJsonlSessionStore(pathOrOptions) {
4
5
  const options = typeof pathOrOptions === "string" ? { path: pathOrOptions, createDirectory: true } : pathOrOptions;
5
6
  const path = options.path;
6
7
  let appendChain = Promise.resolve();
8
+ // ponytail: single-process only. The appendChain serializes appends within one
9
+ // process; there is no cross-process lock, so two processes writing the same
10
+ // file can still race. Multi-writer safety is host-owned (DB adapter or external
11
+ // lock). The expectedParentId/idempotency guards below mirror the memory store;
12
+ // a DB adapter enforces them via a conditional transaction + unique index.
13
+ const idempotencySeen = new Set();
7
14
  return {
8
- append(entry) {
15
+ append(entry, appendOptions) {
9
16
  appendChain = appendChain.then(async () => {
10
17
  if (options.createDirectory !== false)
11
18
  await mkdir(dirname(path), { recursive: true });
12
- if (await findEntry(path, entry.id))
19
+ const entries = await readEntries(path);
20
+ const dedupKey = appendOptions?.idempotencyKey
21
+ ? `${entry.sessionId}\u0000${appendOptions.idempotencyKey}\u0000${appendOptions.expectedParentId ?? ""}`
22
+ : undefined;
23
+ if (dedupKey !== undefined && idempotencySeen.has(dedupKey)) {
24
+ throw new SessionAppendConflictError({ code: SESSION_APPEND_CONFLICT_CODE, idempotencyDuplicate: true });
25
+ }
26
+ if (appendOptions?.expectedParentId !== undefined && !entries.some((existing) => existing.id === appendOptions.expectedParentId)) {
27
+ throw new SessionAppendConflictError({ code: SESSION_APPEND_CONFLICT_CODE, expectedParentId: appendOptions.expectedParentId });
28
+ }
29
+ if (entries.some((existing) => existing.id === entry.id))
13
30
  throw new Error(`Duplicate session entry id: ${entry.id}`);
31
+ if (dedupKey !== undefined)
32
+ idempotencySeen.add(dedupKey);
14
33
  await appendFile(path, `${JSON.stringify(entry)}\n`, "utf8");
15
34
  });
16
35
  return appendChain;
@@ -71,7 +90,15 @@ function validateSessionEntry(value, lineNumber, raw) {
71
90
  if (entry.parentId !== undefined && typeof entry.parentId !== "string") {
72
91
  return { ok: false, error: { line: lineNumber, message: "Invalid parentId: expected string", raw } };
73
92
  }
74
- switch (entry.kind) {
93
+ const schemaVersion = entry.schemaVersion ?? SESSION_ENTRY_SCHEMA_VERSION;
94
+ if (typeof schemaVersion !== "number" || schemaVersion !== SESSION_ENTRY_SCHEMA_VERSION) {
95
+ return { ok: false, error: { line: lineNumber, message: `Unsupported session entry schema version: ${schemaVersion}`, raw } };
96
+ }
97
+ const kind = entry.kind;
98
+ if (!isSessionEntryKind(kind)) {
99
+ return { ok: false, error: { line: lineNumber, message: `Unknown session entry kind: ${kind}`, raw } };
100
+ }
101
+ switch (kind) {
75
102
  case "message":
76
103
  if (!isMessage(entry.message))
77
104
  return { ok: false, error: { line: lineNumber, message: "Invalid message entry: expected Message object", raw } };
@@ -98,8 +125,16 @@ function validateSessionEntry(value, lineNumber, raw) {
98
125
  if (typeof entry.label !== "string")
99
126
  return { ok: false, error: { line: lineNumber, message: "Invalid label entry: expected string label", raw } };
100
127
  break;
128
+ case "event":
129
+ if (!isAgentEvent(entry.event))
130
+ return { ok: false, error: { line: lineNumber, message: "Invalid event entry: expected AgentEvent object", raw } };
131
+ break;
132
+ case "metadata":
133
+ if (!isPlainObject(entry.data))
134
+ return { ok: false, error: { line: lineNumber, message: "Invalid metadata entry: expected object data", raw } };
135
+ break;
101
136
  }
102
- return { ok: true, entry };
137
+ return { ok: true, entry: entry };
103
138
  }
104
139
  function isBasicSessionEntry(value) {
105
140
  if (!value || typeof value !== "object" || Array.isArray(value))
@@ -122,6 +157,9 @@ function isModelConfig(value) {
122
157
  const model = value;
123
158
  return typeof model.provider === "string" && typeof model.model === "string";
124
159
  }
160
+ function isAgentEvent(value) {
161
+ return isPlainObject(value) && typeof value.type === "string";
162
+ }
125
163
  function isPlainObject(value) {
126
164
  return !!value && typeof value === "object" && !Array.isArray(value);
127
165
  }
@@ -0,0 +1,30 @@
1
+ import type { SystemPromptContribution } from "../contracts.js";
2
+ import type { PermissionPolicy, TrustPolicy } from "../security.js";
3
+ export interface SystemPromptFilesOptions {
4
+ /** Workspace root. Reads `<workspaceRoot>/AGENTS.md` (project prompt, `source: "app"`).
5
+ * Trust-gated via `trust` (untrusted workspace → skipped silently, fail-closed). */
6
+ readonly workspaceRoot?: string;
7
+ /** Global root. Reads `<globalRoot>/.prism/agent/SYSTEM.md` (user/global prompt, `source: "user"`).
8
+ * User-owned: no workspace trust check; its presence is the user's explicit choice. */
9
+ readonly globalRoot?: string;
10
+ /** Override path for `AGENTS.md` (e.g. `--agents-md-file`). Still `source: "app"`, still trust-gated;
11
+ * the caller builds `trust` with the file's parent in its trusted roots so the explicit opt-in is honored. */
12
+ readonly agentsMdPath?: string;
13
+ /** Override path for `SYSTEM.md` (e.g. `--system-md-file`). Still `source: "user"`, no trust gate (user-owned). */
14
+ readonly systemMdPath?: string;
15
+ readonly trust?: TrustPolicy;
16
+ readonly permission?: PermissionPolicy;
17
+ }
18
+ /** Load the standard system/project prompt files as `SystemPromptContribution` layers,
19
+ * feeding the existing `composeSystemPrompt` pipeline (no parallel mechanism). Returns
20
+ * `SYSTEM.md` first (`source: "user"`, the Phase 31 base layer) then `AGENTS.md`
21
+ * (`source: "app"`); the rank order is enforced by `composeSystemPrompt`'s `sourceRank`,
22
+ * so input order here only matters for the stable tie-break when sources collide.
23
+ *
24
+ * Standalone SDK use (no `workspaceRoot`/`globalRoot`/override paths) returns `[]` and
25
+ * performs no filesystem I/O — `AgentConfig.instructions`/`systemPrompt` keep working unchanged.
26
+ *
27
+ * ponytail: two `readFile` calls max, no `readdir`, no scan, no `import()`. Root-level
28
+ * singletons don't fit `discoverContributions`' named-subdir scanner, so this is a
29
+ * sibling loader mirroring `src/node/instruction-injectors.ts`'s per-concern adapter shape. */
30
+ export declare function loadSystemPromptFiles(options: SystemPromptFilesOptions): Promise<readonly SystemPromptContribution[]>;
@@ -0,0 +1,53 @@
1
+ import { join } from "node:path";
2
+ import { assertPermission, isTrusted } from "../security.js";
3
+ import { readOptionalFile } from "./contribution-discovery.js";
4
+ /** Load the standard system/project prompt files as `SystemPromptContribution` layers,
5
+ * feeding the existing `composeSystemPrompt` pipeline (no parallel mechanism). Returns
6
+ * `SYSTEM.md` first (`source: "user"`, the Phase 31 base layer) then `AGENTS.md`
7
+ * (`source: "app"`); the rank order is enforced by `composeSystemPrompt`'s `sourceRank`,
8
+ * so input order here only matters for the stable tie-break when sources collide.
9
+ *
10
+ * Standalone SDK use (no `workspaceRoot`/`globalRoot`/override paths) returns `[]` and
11
+ * performs no filesystem I/O — `AgentConfig.instructions`/`systemPrompt` keep working unchanged.
12
+ *
13
+ * ponytail: two `readFile` calls max, no `readdir`, no scan, no `import()`. Root-level
14
+ * singletons don't fit `discoverContributions`' named-subdir scanner, so this is a
15
+ * sibling loader mirroring `src/node/instruction-injectors.ts`'s per-concern adapter shape. */
16
+ export async function loadSystemPromptFiles(options) {
17
+ const agentsPath = options.agentsMdPath
18
+ ?? (options.workspaceRoot !== undefined ? join(options.workspaceRoot, "AGENTS.md") : undefined);
19
+ const systemPath = options.systemMdPath
20
+ ?? (options.globalRoot !== undefined ? join(options.globalRoot, ".prism", "agent", "SYSTEM.md") : undefined);
21
+ const out = [];
22
+ if (systemPath !== undefined) {
23
+ const layer = await readSystemFile(systemPath, options.permission);
24
+ if (layer)
25
+ out.push(layer);
26
+ }
27
+ if (agentsPath !== undefined) {
28
+ const layer = await readAgentsFile(agentsPath, options.trust, options.permission);
29
+ if (layer)
30
+ out.push(layer);
31
+ }
32
+ return out;
33
+ }
34
+ async function readSystemFile(path, permission) {
35
+ await assertPermission(permission, { kind: "resource", action: "load", target: path });
36
+ const text = await readOptionalFile(path);
37
+ if (text === undefined)
38
+ return undefined;
39
+ return { id: "system-md", source: "user", mode: "append", text };
40
+ }
41
+ async function readAgentsFile(path, trust, permission) {
42
+ // ponytail: trust gate mirrors discoverContributions — untrusted AGENTS.md is skipped silently
43
+ // (fail-closed, no throw). createPathTrustPolicy resolves symlinks internally, so a symlinked
44
+ // AGENTS.md escaping the trusted root fails containment inside the policy.
45
+ if (!(await isTrusted(trust, { kind: "project", target: path })))
46
+ return undefined;
47
+ await assertPermission(permission, { kind: "resource", action: "load", target: path });
48
+ const text = await readOptionalFile(path);
49
+ if (text === undefined)
50
+ return undefined;
51
+ return { id: "agents-md", source: "app", mode: "append", text };
52
+ }
53
+ //# sourceMappingURL=system-project-prompts.js.map
@@ -1,4 +1,4 @@
1
- import type { ContentBlock, JsonObject, ProviderEvent, ToolCallContent, Usage } from "./contracts.js";
1
+ import type { ContentBlock, JsonObject, ProviderEvent, ToolCallContent, ToolCallDeltaContent, Usage } from "./contracts.js";
2
2
  export declare function providerTextDelta(text: string): ProviderEvent;
3
3
  export declare function providerThinkingDelta(text: string, signature?: string): ProviderEvent;
4
4
  export declare function providerContentDelta(content: ContentBlock): ProviderEvent;
@@ -9,6 +9,8 @@ export declare function providerToolCallDelta(delta: {
9
9
  readonly name?: string;
10
10
  readonly argumentsText?: string;
11
11
  }): ProviderEvent;
12
+ export declare function providerToolCallDeltaContent(delta: Omit<ToolCallDeltaContent, "type">): ToolCallDeltaContent;
13
+ export declare function reconstructToolCallDeltas(events: readonly ProviderEvent[]): readonly ToolCallContent[];
12
14
  export declare function providerUsage(usage: Usage): ProviderEvent;
13
15
  export declare function providerDone(usage?: Usage): ProviderEvent;
14
16
  export declare function providerError(error: unknown, secrets?: readonly (string | undefined)[]): ProviderEvent;
@@ -14,6 +14,29 @@ export function providerToolCall(call) {
14
14
  export function providerToolCallDelta(delta) {
15
15
  return { type: "tool_call_delta", ...delta };
16
16
  }
17
+ export function providerToolCallDeltaContent(delta) {
18
+ return { type: "tool_call_delta", ...delta };
19
+ }
20
+ export function reconstructToolCallDeltas(events) {
21
+ const partials = new Map();
22
+ for (const event of events) {
23
+ if (event.type !== "tool_call_delta")
24
+ continue;
25
+ const partial = partials.get(event.index) ?? { argumentsText: "" };
26
+ if (event.id !== undefined)
27
+ partial.id = event.id;
28
+ if (event.name !== undefined)
29
+ partial.name = event.name;
30
+ if (event.argumentsText !== undefined)
31
+ partial.argumentsText += event.argumentsText;
32
+ partials.set(event.index, partial);
33
+ }
34
+ return [...partials.entries()].sort(([a], [b]) => a - b).map(([index, partial]) => {
35
+ if (!partial.id || !partial.name)
36
+ throw new Error(`Incomplete tool call delta at index ${index}`);
37
+ return toolCallContent(partial.id, partial.name, parseToolCallArguments(partial.argumentsText, index));
38
+ });
39
+ }
17
40
  export function providerUsage(usage) {
18
41
  return { type: "usage", usage };
19
42
  }
@@ -27,4 +50,15 @@ export function providerError(error, secrets = []) {
27
50
  export function toolCallContent(id, name, args = {}) {
28
51
  return { type: "tool_call", id, name, arguments: args };
29
52
  }
53
+ function parseToolCallArguments(text, index) {
54
+ try {
55
+ const value = text ? JSON.parse(text) : {};
56
+ if (!value || typeof value !== "object" || Array.isArray(value))
57
+ throw new Error("not object");
58
+ return value;
59
+ }
60
+ catch (error) {
61
+ throw new Error(`Invalid tool call arguments at index ${index}: ${error instanceof Error ? error.message : String(error)}`);
62
+ }
63
+ }
30
64
  //# sourceMappingURL=provider-events.js.map
@@ -14,6 +14,7 @@ export function createProviderRequestPolicyChain(policies) {
14
14
  },
15
15
  };
16
16
  }
17
+ // Legacy cacheKey/cacheRetention policy; structured cache hints are merged separately.
17
18
  export function createSessionCachePolicy(options = {}) {
18
19
  return {
19
20
  name: "session-cache",
@@ -35,13 +36,26 @@ export function mergeProviderRequestOptions(base, patch) {
35
36
  return patch;
36
37
  if (!patch)
37
38
  return base;
38
- return {
39
+ const merged = {
39
40
  ...base,
40
41
  ...patch,
41
42
  headers: patch.headers || base.headers ? { ...base.headers, ...patch.headers } : undefined,
42
43
  compat: patch.compat || base.compat ? { ...base.compat, ...patch.compat } : undefined,
43
44
  extra: patch.extra || base.extra ? { ...base.extra, ...patch.extra } : undefined,
44
45
  };
46
+ if (!patch.cache && !base.cache)
47
+ return merged;
48
+ return {
49
+ ...merged,
50
+ cache: {
51
+ ...base.cache,
52
+ ...patch.cache,
53
+ breakpoints: base.cache?.breakpoints || patch.cache?.breakpoints ? [
54
+ ...(base.cache?.breakpoints ?? []),
55
+ ...(patch.cache?.breakpoints ?? []),
56
+ ] : undefined,
57
+ },
58
+ };
45
59
  }
46
60
  export function normalizeProviderRequestPolicyResult(result) {
47
61
  return "request" in result ? result : { request: result };
@@ -17,8 +17,8 @@ export function createOpenAICompatibleProvider(options) {
17
17
  const response = await fetchImpl(`${options.baseUrl.replace(/\/$/, "")}/chat/completions`, {
18
18
  method: "POST",
19
19
  headers: {
20
- "content-type": "application/json",
21
20
  ...request.options?.headers,
21
+ "content-type": "application/json",
22
22
  ...(apiKey ? { authorization: `Bearer ${apiKey}` } : {}),
23
23
  },
24
24
  body: JSON.stringify(toOpenAIRequest(request)),
@@ -1,8 +1,12 @@
1
- import type { AIProvider, ModelConfig } from "./contracts.js";
1
+ import type { AIProvider, ModelConfig, ProviderResolver } from "./contracts.js";
2
+ import { type DuplicateRegistrationOptions } from "./registry-options.js";
2
3
  export interface ProviderRegistry {
3
4
  register(provider: AIProvider): void;
4
5
  get(id: string): AIProvider | undefined;
5
6
  resolve(model: Pick<ModelConfig, "provider"> | string): AIProvider;
6
7
  list(): readonly AIProvider[];
7
8
  }
8
- export declare function createProviderRegistry(providers?: readonly AIProvider[]): ProviderRegistry;
9
+ export interface ProviderRegistryOptions extends DuplicateRegistrationOptions {
10
+ }
11
+ export declare function createProviderRegistry(providers?: readonly AIProvider[], options?: ProviderRegistryOptions): ProviderRegistry;
12
+ export declare function createProviderResolver(source: ProviderRegistry | readonly AIProvider[]): ProviderResolver;
package/dist/providers.js CHANGED
@@ -1,7 +1,9 @@
1
- export function createProviderRegistry(providers = []) {
1
+ import { assertCanRegister } from "./registry-options.js";
2
+ export function createProviderRegistry(providers = [], options = {}) {
2
3
  const byId = new Map();
3
4
  const registry = {
4
5
  register(provider) {
6
+ assertCanRegister(byId, provider.id, "provider", provider.id, options.duplicate);
5
7
  byId.set(provider.id, provider);
6
8
  },
7
9
  get(id) {
@@ -22,4 +24,16 @@ export function createProviderRegistry(providers = []) {
22
24
  registry.register(provider);
23
25
  return registry;
24
26
  }
27
+ function isProviderRegistry(source) {
28
+ return !Array.isArray(source);
29
+ }
30
+ export function createProviderResolver(source) {
31
+ // ponytail: array source builds the lookup map once at construction; registry
32
+ // source reuses ProviderRegistry.get so there is one lookup implementation.
33
+ if (isProviderRegistry(source)) {
34
+ return (model) => source.get(model.provider) ?? undefined;
35
+ }
36
+ const lookup = new Map(source.map((p) => [p.id, p]));
37
+ return (model) => lookup.get(model.provider) ?? undefined;
38
+ }
25
39
  //# sourceMappingURL=providers.js.map
@@ -1,4 +1,4 @@
1
- import type { AgentEvent, ErrorInfo, Message, ProviderRequest, SessionEntry } from "./contracts.js";
1
+ import type { AgentEvent, ErrorInfo, Message, ProviderRequest, RunLedgerRecord, SessionEntry } from "./contracts.js";
2
2
  export interface SecretRedactor {
3
3
  redact<T>(value: T): T;
4
4
  }
@@ -7,5 +7,6 @@ export declare function redactMessage(message: Message, redactor?: SecretRedacto
7
7
  export declare function redactAgentEvent(event: AgentEvent, redactor?: SecretRedactor): AgentEvent;
8
8
  export declare function redactSessionEntry(entry: SessionEntry, redactor?: SecretRedactor): SessionEntry;
9
9
  export declare function redactProviderRequest(request: ProviderRequest, redactor?: SecretRedactor): ProviderRequest;
10
+ export declare function redactRunLedgerRecord<T extends RunLedgerRecord>(record: T, redactor?: SecretRedactor): T;
10
11
  export declare function redactSecrets<T>(value: T, secrets: readonly (string | undefined)[]): T;
11
12
  export declare function errorToErrorInfo(error: unknown, secrets?: readonly (string | undefined)[]): ErrorInfo;
package/dist/redaction.js CHANGED
@@ -14,6 +14,9 @@ export function redactSessionEntry(entry, redactor) {
14
14
  export function redactProviderRequest(request, redactor) {
15
15
  return redactor?.redact(request) ?? request;
16
16
  }
17
+ export function redactRunLedgerRecord(record, redactor) {
18
+ return redactor?.redact(record) ?? record;
19
+ }
17
20
  export function redactSecrets(value, secrets) {
18
21
  const needles = secrets.filter((secret) => Boolean(secret));
19
22
  if (needles.length === 0)
@@ -0,0 +1,5 @@
1
+ export type DuplicateRegistrationPolicy = "replace" | "error";
2
+ export interface DuplicateRegistrationOptions {
3
+ readonly duplicate?: DuplicateRegistrationPolicy;
4
+ }
5
+ export declare function assertCanRegister<K>(map: ReadonlyMap<K, unknown>, key: K, label: string, displayKey?: string, duplicate?: DuplicateRegistrationPolicy): void;
@@ -0,0 +1,5 @@
1
+ export function assertCanRegister(map, key, label, displayKey = String(key), duplicate = "replace") {
2
+ if (duplicate === "error" && map.has(key))
3
+ throw new Error(`Duplicate ${label}: ${displayKey}`);
4
+ }
5
+ //# sourceMappingURL=registry-options.js.map