@tangle-network/agent-app 0.18.0 → 0.20.0

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.
@@ -0,0 +1,76 @@
1
+ // src/model-resolution/index.ts
2
+ function canonicalModelId(model) {
3
+ if (model.id.includes("/")) return model.id;
4
+ const provider = model._provider ?? model.provider;
5
+ return provider ? `${provider}/${model.id}` : model.id;
6
+ }
7
+ function resolveChatModel(input) {
8
+ const request = cleanModelId(input.requestModel);
9
+ if (request) return { model: request, source: "request" };
10
+ const workspace = cleanModelId(input.workspaceModel);
11
+ if (workspace) return { model: workspace, source: "workspace" };
12
+ const env = cleanModelId(input.envModel);
13
+ if (env) return { model: env, source: "env" };
14
+ return { model: input.defaultModel, source: "default" };
15
+ }
16
+ async function validateChatModelId(modelId, input) {
17
+ const cleaned = cleanModelId(modelId);
18
+ if (!cleaned) return { succeeded: false, error: "Model id must be a non-empty string." };
19
+ if (!isWellFormedModelId(cleaned)) return { succeeded: false, error: `Model id is malformed: ${cleaned}` };
20
+ const allowed = new Set(input.allowlist ?? []);
21
+ if (allowed.has(cleaned)) return { succeeded: true, value: cleaned };
22
+ if (cleanModelId(input.envModel) === cleaned) return { succeeded: true, value: cleaned };
23
+ if (!input.loadModels || typeof input.routerBaseUrl !== "string" || input.routerBaseUrl.length === 0) {
24
+ return { succeeded: false, error: `Model is not available: ${cleaned}` };
25
+ }
26
+ let catalog;
27
+ try {
28
+ catalog = await input.loadModels(input.routerBaseUrl);
29
+ } catch (err) {
30
+ const message = err instanceof Error ? err.message : String(err);
31
+ return { succeeded: false, error: `Could not validate model catalog: ${message}` };
32
+ }
33
+ const ids = new Set(catalog.flatMap(catalogIdsForModel));
34
+ if (ids.has(cleaned)) return { succeeded: true, value: cleaned };
35
+ if (!cleaned.includes("/")) {
36
+ const canonicalBySuffix = /* @__PURE__ */ new Map();
37
+ for (const model of catalog) {
38
+ if (typeof model.id !== "string" || !model.id.trim()) continue;
39
+ const canonical = canonicalModelId(model);
40
+ if (!canonical.includes("/")) continue;
41
+ const suffix = canonical.split("/").slice(1).join("/");
42
+ const entries = canonicalBySuffix.get(suffix);
43
+ if (entries) entries.push(canonical);
44
+ else canonicalBySuffix.set(suffix, [canonical]);
45
+ }
46
+ const matches = canonicalBySuffix.get(cleaned);
47
+ if (matches && matches.length === 1) return { succeeded: true, value: matches[0] };
48
+ }
49
+ return { succeeded: false, error: `Model is not available: ${cleaned}` };
50
+ }
51
+ function cleanModelId(value) {
52
+ if (typeof value !== "string") return void 0;
53
+ const trimmed = value.trim();
54
+ return trimmed.length > 0 ? trimmed : void 0;
55
+ }
56
+ function isWellFormedModelId(modelId) {
57
+ if (modelId.length > 200) return false;
58
+ return /^[A-Za-z0-9._/@:-]+$/.test(modelId);
59
+ }
60
+ function catalogIdsForModel(model) {
61
+ const ids = /* @__PURE__ */ new Set();
62
+ if (typeof model.id === "string" && model.id.trim()) ids.add(model.id.trim());
63
+ if (typeof model.id === "string" && model.id.trim() && !model.id.includes("/")) {
64
+ const canonical = canonicalModelId(model);
65
+ if (canonical.includes("/")) ids.add(canonical);
66
+ }
67
+ return [...ids];
68
+ }
69
+ export {
70
+ catalogIdsForModel,
71
+ cleanModelId,
72
+ isWellFormedModelId,
73
+ resolveChatModel,
74
+ validateChatModelId
75
+ };
76
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/model-resolution/index.ts"],"sourcesContent":["/**\n * Canonical chat-model resolution — identical across every agent app.\n *\n * The ONLY per-app inputs are DATA, never logic: the default model, the\n * allowlist, the env value the deployment set, and the catalog-fetch loader.\n * The logic is one precedence ladder + one fail-closed validator that every\n * product uses the same way — there is no per-product variant, no env-var name\n * baked in, and no backend dimension (router-vs-sandbox is the harness/dispatch\n * concern, not model resolution; a sandbox's provider default lives in the\n * sandbox subpath).\n *\n * - resolveChatModel: request > workspace > env > default. The product reads its\n * own deploy env var and passes the VALUE as `envModel`; the shell knows no\n * env-var names. Source is canonical: 'request' | 'workspace' | 'env' | 'default'.\n * - validateChatModelId: fail-closed. Admit an id that is in the allowlist, or\n * equals the operator-set env model, or is served by the live router catalog\n * (exact, or a bare id resolved to its canonical id when the suffix is unique).\n */\n\n/** The router /v1/models entry shape this module reads. Minimal on purpose. */\nexport interface ModelInfo {\n id: string\n name?: string\n _provider?: string\n provider?: string\n}\n\n/** Canonical (provider-prefixed) id for a catalog entry: pass through an id that\n * already carries a provider, else prefix the entry's provider when present. */\nfunction canonicalModelId(model: ModelInfo): string {\n if (model.id.includes('/')) return model.id\n const provider = model._provider ?? model.provider\n return provider ? `${provider}/${model.id}` : model.id\n}\n\nexport type ChatModelSource = 'request' | 'workspace' | 'env' | 'default'\n\nexport interface ResolvedChatModel {\n model: string\n source: ChatModelSource\n}\n\nexport interface ChatModelValidationSuccess {\n succeeded: true\n value: string\n}\n\nexport interface ChatModelValidationFailure {\n succeeded: false\n error: string\n}\n\nexport type ChatModelValidationResult = ChatModelValidationSuccess | ChatModelValidationFailure\n\n/** The catalog-fetch boundary: maps a router base URL to the raw model list. */\nexport type LoadModels = (routerBaseUrl: string) => Promise<ModelInfo[]>\n\nexport interface ResolveChatModelInput {\n /** Per-request override (highest precedence). */\n requestModel?: string\n /** Persisted workspace-pinned model. */\n workspaceModel?: string\n /** The value the deployment's model env var holds (the product reads its own\n * var name and passes the value — the shell stays env-var-name agnostic). */\n envModel?: string\n /** Final fallback (the product's default, typically profile.model.default). */\n defaultModel: string\n}\n\n/** Resolve the chat-turn model by the one canonical precedence. Blank values are\n * treated as absent. */\nexport function resolveChatModel(input: ResolveChatModelInput): ResolvedChatModel {\n const request = cleanModelId(input.requestModel)\n if (request) return { model: request, source: 'request' }\n const workspace = cleanModelId(input.workspaceModel)\n if (workspace) return { model: workspace, source: 'workspace' }\n const env = cleanModelId(input.envModel)\n if (env) return { model: env, source: 'env' }\n return { model: input.defaultModel, source: 'default' }\n}\n\nexport interface ValidateChatModelIdInput {\n /** Ids accepted without a catalog round-trip (defaults + operator-trusted). */\n allowlist?: Iterable<string>\n /** The operator-set env model value — always admitted (operator-trusted). */\n envModel?: string\n /** Catalog loader; required to reach the catalog path. */\n loadModels?: LoadModels\n /** Catalog endpoint base; required to reach the catalog path. */\n routerBaseUrl?: string\n}\n\n/**\n * Fail-closed model-id validation. Accepts an id only when it is well-formed AND\n * (in the allowlist, or equals the operator-set env model, or served by the live\n * catalog). A bare id (no provider prefix) resolves to its canonical id only when\n * the suffix is unique across the catalog — an ambiguous suffix is rejected\n * rather than silently assigned a provider.\n */\nexport async function validateChatModelId(\n modelId: unknown,\n input: ValidateChatModelIdInput,\n): Promise<ChatModelValidationResult> {\n const cleaned = cleanModelId(modelId)\n if (!cleaned) return { succeeded: false, error: 'Model id must be a non-empty string.' }\n if (!isWellFormedModelId(cleaned)) return { succeeded: false, error: `Model id is malformed: ${cleaned}` }\n\n const allowed = new Set(input.allowlist ?? [])\n if (allowed.has(cleaned)) return { succeeded: true, value: cleaned }\n\n // The operator-set env model is trusted without a catalog round-trip.\n if (cleanModelId(input.envModel) === cleaned) return { succeeded: true, value: cleaned }\n\n if (!input.loadModels || typeof input.routerBaseUrl !== 'string' || input.routerBaseUrl.length === 0) {\n return { succeeded: false, error: `Model is not available: ${cleaned}` }\n }\n\n let catalog: ModelInfo[]\n try {\n catalog = await input.loadModels(input.routerBaseUrl)\n } catch (err) {\n const message = err instanceof Error ? err.message : String(err)\n return { succeeded: false, error: `Could not validate model catalog: ${message}` }\n }\n\n const ids = new Set(catalog.flatMap(catalogIdsForModel))\n if (ids.has(cleaned)) return { succeeded: true, value: cleaned }\n\n if (!cleaned.includes('/')) {\n const canonicalBySuffix = new Map<string, string[]>()\n for (const model of catalog) {\n if (typeof model.id !== 'string' || !model.id.trim()) continue\n const canonical = canonicalModelId(model)\n if (!canonical.includes('/')) continue\n const suffix = canonical.split('/').slice(1).join('/')\n const entries = canonicalBySuffix.get(suffix)\n if (entries) entries.push(canonical)\n else canonicalBySuffix.set(suffix, [canonical])\n }\n const matches = canonicalBySuffix.get(cleaned)\n if (matches && matches.length === 1) return { succeeded: true, value: matches[0]! }\n }\n\n return { succeeded: false, error: `Model is not available: ${cleaned}` }\n}\n\nexport function cleanModelId(value: unknown): string | undefined {\n if (typeof value !== 'string') return undefined\n const trimmed = value.trim()\n return trimmed.length > 0 ? trimmed : undefined\n}\n\nexport function isWellFormedModelId(modelId: string): boolean {\n if (modelId.length > 200) return false\n return /^[A-Za-z0-9._/@:-]+$/.test(modelId)\n}\n\nexport function catalogIdsForModel(model: ModelInfo): string[] {\n const ids = new Set<string>()\n if (typeof model.id === 'string' && model.id.trim()) ids.add(model.id.trim())\n if (typeof model.id === 'string' && model.id.trim() && !model.id.includes('/')) {\n const canonical = canonicalModelId(model)\n if (canonical.includes('/')) ids.add(canonical)\n }\n return [...ids]\n}\n"],"mappings":";AA6BA,SAAS,iBAAiB,OAA0B;AAClD,MAAI,MAAM,GAAG,SAAS,GAAG,EAAG,QAAO,MAAM;AACzC,QAAM,WAAW,MAAM,aAAa,MAAM;AAC1C,SAAO,WAAW,GAAG,QAAQ,IAAI,MAAM,EAAE,KAAK,MAAM;AACtD;AAsCO,SAAS,iBAAiB,OAAiD;AAChF,QAAM,UAAU,aAAa,MAAM,YAAY;AAC/C,MAAI,QAAS,QAAO,EAAE,OAAO,SAAS,QAAQ,UAAU;AACxD,QAAM,YAAY,aAAa,MAAM,cAAc;AACnD,MAAI,UAAW,QAAO,EAAE,OAAO,WAAW,QAAQ,YAAY;AAC9D,QAAM,MAAM,aAAa,MAAM,QAAQ;AACvC,MAAI,IAAK,QAAO,EAAE,OAAO,KAAK,QAAQ,MAAM;AAC5C,SAAO,EAAE,OAAO,MAAM,cAAc,QAAQ,UAAU;AACxD;AAoBA,eAAsB,oBACpB,SACA,OACoC;AACpC,QAAM,UAAU,aAAa,OAAO;AACpC,MAAI,CAAC,QAAS,QAAO,EAAE,WAAW,OAAO,OAAO,uCAAuC;AACvF,MAAI,CAAC,oBAAoB,OAAO,EAAG,QAAO,EAAE,WAAW,OAAO,OAAO,0BAA0B,OAAO,GAAG;AAEzG,QAAM,UAAU,IAAI,IAAI,MAAM,aAAa,CAAC,CAAC;AAC7C,MAAI,QAAQ,IAAI,OAAO,EAAG,QAAO,EAAE,WAAW,MAAM,OAAO,QAAQ;AAGnE,MAAI,aAAa,MAAM,QAAQ,MAAM,QAAS,QAAO,EAAE,WAAW,MAAM,OAAO,QAAQ;AAEvF,MAAI,CAAC,MAAM,cAAc,OAAO,MAAM,kBAAkB,YAAY,MAAM,cAAc,WAAW,GAAG;AACpG,WAAO,EAAE,WAAW,OAAO,OAAO,2BAA2B,OAAO,GAAG;AAAA,EACzE;AAEA,MAAI;AACJ,MAAI;AACF,cAAU,MAAM,MAAM,WAAW,MAAM,aAAa;AAAA,EACtD,SAAS,KAAK;AACZ,UAAM,UAAU,eAAe,QAAQ,IAAI,UAAU,OAAO,GAAG;AAC/D,WAAO,EAAE,WAAW,OAAO,OAAO,qCAAqC,OAAO,GAAG;AAAA,EACnF;AAEA,QAAM,MAAM,IAAI,IAAI,QAAQ,QAAQ,kBAAkB,CAAC;AACvD,MAAI,IAAI,IAAI,OAAO,EAAG,QAAO,EAAE,WAAW,MAAM,OAAO,QAAQ;AAE/D,MAAI,CAAC,QAAQ,SAAS,GAAG,GAAG;AAC1B,UAAM,oBAAoB,oBAAI,IAAsB;AACpD,eAAW,SAAS,SAAS;AAC3B,UAAI,OAAO,MAAM,OAAO,YAAY,CAAC,MAAM,GAAG,KAAK,EAAG;AACtD,YAAM,YAAY,iBAAiB,KAAK;AACxC,UAAI,CAAC,UAAU,SAAS,GAAG,EAAG;AAC9B,YAAM,SAAS,UAAU,MAAM,GAAG,EAAE,MAAM,CAAC,EAAE,KAAK,GAAG;AACrD,YAAM,UAAU,kBAAkB,IAAI,MAAM;AAC5C,UAAI,QAAS,SAAQ,KAAK,SAAS;AAAA,UAC9B,mBAAkB,IAAI,QAAQ,CAAC,SAAS,CAAC;AAAA,IAChD;AACA,UAAM,UAAU,kBAAkB,IAAI,OAAO;AAC7C,QAAI,WAAW,QAAQ,WAAW,EAAG,QAAO,EAAE,WAAW,MAAM,OAAO,QAAQ,CAAC,EAAG;AAAA,EACpF;AAEA,SAAO,EAAE,WAAW,OAAO,OAAO,2BAA2B,OAAO,GAAG;AACzE;AAEO,SAAS,aAAa,OAAoC;AAC/D,MAAI,OAAO,UAAU,SAAU,QAAO;AACtC,QAAM,UAAU,MAAM,KAAK;AAC3B,SAAO,QAAQ,SAAS,IAAI,UAAU;AACxC;AAEO,SAAS,oBAAoB,SAA0B;AAC5D,MAAI,QAAQ,SAAS,IAAK,QAAO;AACjC,SAAO,uBAAuB,KAAK,OAAO;AAC5C;AAEO,SAAS,mBAAmB,OAA4B;AAC7D,QAAM,MAAM,oBAAI,IAAY;AAC5B,MAAI,OAAO,MAAM,OAAO,YAAY,MAAM,GAAG,KAAK,EAAG,KAAI,IAAI,MAAM,GAAG,KAAK,CAAC;AAC5E,MAAI,OAAO,MAAM,OAAO,YAAY,MAAM,GAAG,KAAK,KAAK,CAAC,MAAM,GAAG,SAAS,GAAG,GAAG;AAC9E,UAAM,YAAY,iBAAiB,KAAK;AACxC,QAAI,UAAU,SAAS,GAAG,EAAG,KAAI,IAAI,SAAS;AAAA,EAChD;AACA,SAAO,CAAC,GAAG,GAAG;AAChB;","names":[]}
@@ -0,0 +1,149 @@
1
+ import { AgentProfileFileMount, AgentProfileMcpServer, AgentProfile } from '@tangle-network/sandbox';
2
+ import { profile } from '@tangle-network/agent-eval';
3
+ export { profile } from '@tangle-network/agent-eval';
4
+ import { SkillEntry } from '../skills/index.js';
5
+ export { ComposeShellResourcesInput, CorpusEntry, CorpusLoadResult, GlobModules, LoadCorpusOptions, composeShellResources, corpusSkills, loadMarkdownCorpus, registrySkills, skillMountPath } from '../skills/index.js';
6
+
7
+ /**
8
+ * Profile composer + evolvable-section seam for agent products.
9
+ *
10
+ * The standard "load a deployable AgentProfile, including skills, plus the
11
+ * skills the end user added to their own instance" entry point. A product holds
12
+ * a canonical base `AgentProfile` (role/environment/tool-conventions rendered
13
+ * into `prompt.systemPrompt`, baseline skills, baseline MCP). At deploy/turn
14
+ * time it layers four file-mount channels onto `resources.files` —
15
+ *
16
+ * 1. skills — the always-mounted product skill corpus
17
+ * 2. knowledge — a second always-mounted corpus (domain knowledge pack)
18
+ * 3. registry — the tier-gated installable registry (free -> boot-mounted)
19
+ * 4. userSkills — per-user / per-workspace skills the END USER adds to their
20
+ * own instance, mounted at `~/.claude/skills/<id>/SKILL.md`
21
+ * exactly like the registry's free tier
22
+ *
23
+ * plus an optional MCP overlay (delegation + per-turn app-tool side channel), a
24
+ * per-turn `systemPrompt` override, and a `name` override. The merge is the SDK
25
+ * `mergeAgentProfiles`: `mcp` is last-wins per key (base -> overlay), `resources`
26
+ * arrays are concatenated (base ++ overlay), `prompt` is shallow-merged so an
27
+ * overlay carrying only `systemPrompt` overrides it while keeping base
28
+ * instructions. The compose algebra is DATA — the product injects the base
29
+ * profile, the channel mounts (built with the `skills` subpath primitives), the
30
+ * delegation/app-tool MCP map, and the override strings; nothing here reaches
31
+ * for env, a glob, or a specific product's profile.
32
+ *
33
+ * The evolvable-section seam is the loader closure. A product's single
34
+ * self-improvable domain section (the one `applyDomainPatch` targets) loads its
35
+ * body from a deployed markdown override, falling back to an in-tree baseline.
36
+ * The `import.meta.glob('<lit>', ...)` literal must stay at the CONSUMER call
37
+ * site (Vite static-analyzes it), so `makeEvolvableSection` takes the loader as
38
+ * a closure and a REQUIRED `baseline` — it never constructs a glob and never
39
+ * defaults the baseline, so a product can't render an empty learned-guidance
40
+ * section. `stripComments` is the shared "is this addendum really empty?" test.
41
+ */
42
+
43
+ /** The file-mount channels layered onto `resources.files`. The first three
44
+ * mirror {@link ComposeShellResourcesInput}; `userSkills` is the per-user /
45
+ * per-workspace channel — skills the END USER added to their own instance,
46
+ * mounted at the harness skill-discovery path like the registry's free tier. */
47
+ interface ProfileChannels {
48
+ /** Always-mounted skill corpus (pass `corpusSkills(...)`). */
49
+ skills?: AgentProfileFileMount[];
50
+ /** Always-mounted knowledge corpus (pass `corpusSkills(...)` for the pack). */
51
+ knowledge?: AgentProfileFileMount[];
52
+ /** Single-file evolvable / learned-guidance corpora, if mounted as files. */
53
+ evolvable?: AgentProfileFileMount[];
54
+ /** Tier-gated installable registry (pass the registry array; free tier is
55
+ * mounted, paid is install-on-demand). Gated through {@link registrySkills}. */
56
+ registry?: SkillEntry[];
57
+ /** Per-user / per-workspace skills the end user adds to their own instance.
58
+ * Mounted at `~/.claude/skills/<id>/SKILL.md`, the same harness path the
59
+ * registry uses, so a user skill and a registry skill with the same id
60
+ * collide deterministically (the user skill, appended last, wins). */
61
+ userSkills?: UserSkill[];
62
+ /** Final skip filter applied to the composed mount list by mount `path`. */
63
+ filesPredicate?: (mount: AgentProfileFileMount) => boolean;
64
+ }
65
+ /** A per-user / per-workspace skill: an id and an inline `SKILL.md` body. The
66
+ * user-facing analogue of a registry {@link SkillEntry} with no tier gate —
67
+ * every user skill is mounted (the user opted in by adding it). */
68
+ interface UserSkill {
69
+ id: string;
70
+ /** Inline `SKILL.md` body mounted at {@link skillMountPath}. */
71
+ skillMd: string;
72
+ }
73
+ /** Overlay overrides applied on top of the channel mounts. */
74
+ interface ProfileOverlay {
75
+ /** Extra MCP servers merged into the profile `mcp` map (last-wins per key over
76
+ * the base servers). The product builds this from its delegation MCP entry
77
+ * and any per-turn app-tool side-channel servers. An absent/`undefined` entry
78
+ * is dropped — pass only the servers that resolved (fail-closed at the seam,
79
+ * not here). */
80
+ mcp?: Record<string, AgentProfileMcpServer>;
81
+ /** Per-turn system-prompt override. When set, replaces the base
82
+ * `prompt.systemPrompt` while keeping base `prompt.instructions`. When unset,
83
+ * the base prompt passes through unchanged. */
84
+ systemPrompt?: string;
85
+ /** Extra instruction lines merged onto the active prompt (e.g. a per-turn
86
+ * domain/integration directive). Appended to base `prompt.instructions` by
87
+ * the SDK merge. */
88
+ instructions?: string[];
89
+ /** Profile `name` override. When unset, the base name is kept. */
90
+ name?: string;
91
+ }
92
+ /** Project per-user skills onto SDK file mounts at the harness skill-discovery
93
+ * path. No tier gate — a user skill is mounted because the user added it.
94
+ * Sorted by path for determinism (matches {@link registrySkills}). */
95
+ declare function userSkillMounts(userSkills: UserSkill[]): AgentProfileFileMount[];
96
+ /**
97
+ * Compose a deployable `AgentProfile` from a canonical base plus the four
98
+ * file-mount channels and the overlay overrides.
99
+ *
100
+ * Files: base `resources.files` come first; the four channels follow in
101
+ * `skills -> knowledge -> evolvable -> registry -> userSkills` order (so a
102
+ * userSkill that mounts at the same path as a registry skill is the last write
103
+ * and wins). MCP: base servers first, the overlay `mcp` last (last-wins per
104
+ * key). Prompt: the overlay `systemPrompt`, when set, replaces the base one;
105
+ * base instructions are preserved. Name: the overlay `name`, when set, wins.
106
+ *
107
+ * The merge delegates to the SDK `mergeAgentProfiles` (overlay-wins on records,
108
+ * arrays concatenated) — the deterministic algebra is the overlay we hand it,
109
+ * not a hand-rolled spread. `mergeAgentProfiles(base, overlay)` returns
110
+ * `undefined` only when BOTH are `undefined`; `base` is always defined here, so
111
+ * the result is non-`undefined` by construction and we assert that to the caller.
112
+ */
113
+ declare function composeAgentProfile(base: AgentProfile, channels?: ProfileChannels, overlay?: ProfileOverlay): AgentProfile;
114
+ /** True body of an addendum file with HTML comments stripped — an all-comment
115
+ * placeholder counts as empty, so the loader falls back to the baseline. */
116
+ declare function stripComments(raw: string): string;
117
+ /** Inputs to {@link makeEvolvableSection}. */
118
+ interface EvolvableSectionInput {
119
+ /** Section id the self-improvement loop targets with `applyDomainPatch`. */
120
+ id: string;
121
+ /** Section title rendered as `### <title>`. */
122
+ title: string;
123
+ /**
124
+ * Load the deployed section body. The CONSUMER supplies this closure and runs
125
+ * its own `import.meta.glob('<lit>', { eager: true, query: '?raw', import:
126
+ * 'default' })` inside it — the literal must stay at the call site so Vite can
127
+ * static-analyze it; a glob constructed here would not resolve the product's
128
+ * files. Return the raw markdown (comments and all); `makeEvolvableSection`
129
+ * applies {@link stripComments} to decide whether it is really populated.
130
+ */
131
+ load: () => string;
132
+ /**
133
+ * The in-tree fallback body, used when `load()` returns an
134
+ * all-comments/empty placeholder. REQUIRED — no internal default — so a
135
+ * product can never accidentally render an empty evolvable section.
136
+ */
137
+ baseline: string;
138
+ }
139
+ /**
140
+ * Build the one evolvable (`evolvable: true`) domain section whose body comes
141
+ * from the product's loader, falling back to the required baseline when the
142
+ * loaded body is empty after stripping comments. Returns the agent-eval
143
+ * `AgentProfileSection` shape — drop it straight into `prodProfile`'s shipped
144
+ * sections. The loader is the only seam; the empty-vs-populated rule and the
145
+ * baseline fallback are the lifted algebra.
146
+ */
147
+ declare function makeEvolvableSection(input: EvolvableSectionInput): profile.AgentProfileSection;
148
+
149
+ export { type EvolvableSectionInput, type ProfileChannels, type ProfileOverlay, SkillEntry, type UserSkill, composeAgentProfile, makeEvolvableSection, stripComments, userSkillMounts };
@@ -0,0 +1,75 @@
1
+ import {
2
+ composeShellResources,
3
+ corpusSkills,
4
+ loadMarkdownCorpus,
5
+ registrySkills,
6
+ skillMountPath
7
+ } from "../chunk-KOG473C4.js";
8
+
9
+ // src/profile/index.ts
10
+ import { mergeAgentProfiles } from "@tangle-network/sandbox";
11
+ import { profile } from "@tangle-network/agent-eval";
12
+ function userSkillMounts(userSkills) {
13
+ return userSkills.map(
14
+ (s) => ({
15
+ path: skillMountPath(s.id),
16
+ resource: { kind: "inline", name: s.id, content: s.skillMd }
17
+ })
18
+ ).sort((a, b) => a.path.localeCompare(b.path));
19
+ }
20
+ function composeAgentProfile(base, channels = {}, overlay = {}) {
21
+ const shellInput = {
22
+ skills: channels.skills,
23
+ knowledge: channels.knowledge,
24
+ evolvable: channels.evolvable,
25
+ registry: channels.registry ? registrySkills(channels.registry) : void 0,
26
+ predicate: channels.filesPredicate
27
+ };
28
+ const channelFiles = composeShellResources(shellInput);
29
+ const userFiles = channels.userSkills ? userSkillMounts(channels.userSkills) : [];
30
+ const overlayFiles = channels.filesPredicate ? userFiles.filter(channels.filesPredicate) : userFiles;
31
+ const files = [...channelFiles, ...overlayFiles];
32
+ const promptOverlay = {};
33
+ if (overlay.systemPrompt) promptOverlay.systemPrompt = overlay.systemPrompt;
34
+ if (overlay.instructions && overlay.instructions.length > 0) promptOverlay.instructions = overlay.instructions;
35
+ const overlayProfile = {
36
+ ...overlay.name ? { name: overlay.name } : {},
37
+ ...Object.keys(promptOverlay).length > 0 ? { prompt: promptOverlay } : {},
38
+ ...overlay.mcp ? { mcp: overlay.mcp } : {},
39
+ resources: { files }
40
+ };
41
+ const merged = mergeAgentProfiles(base, overlayProfile);
42
+ if (!merged)
43
+ throw new Error("composeAgentProfile: mergeAgentProfiles returned undefined for a defined base");
44
+ return pruneEmptyResourceChannels(merged);
45
+ }
46
+ function pruneEmptyResourceChannels(profile2) {
47
+ if (!profile2.resources) return profile2;
48
+ const kept = Object.fromEntries(
49
+ Object.entries(profile2.resources).filter(([, value]) => !(Array.isArray(value) && value.length === 0))
50
+ );
51
+ const out = { ...profile2, resources: kept };
52
+ if (kept && Object.keys(kept).length === 0) delete out.resources;
53
+ return out;
54
+ }
55
+ function stripComments(raw) {
56
+ return raw.replace(/<!--[\s\S]*?-->/g, "").trim();
57
+ }
58
+ function makeEvolvableSection(input) {
59
+ const loaded = input.load();
60
+ const body = stripComments(loaded) ? loaded.trim() : input.baseline;
61
+ return { id: input.id, title: input.title, body, evolvable: true };
62
+ }
63
+ export {
64
+ composeAgentProfile,
65
+ composeShellResources,
66
+ corpusSkills,
67
+ loadMarkdownCorpus,
68
+ makeEvolvableSection,
69
+ profile,
70
+ registrySkills,
71
+ skillMountPath,
72
+ stripComments,
73
+ userSkillMounts
74
+ };
75
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/profile/index.ts"],"sourcesContent":["/**\n * Profile composer + evolvable-section seam for agent products.\n *\n * The standard \"load a deployable AgentProfile, including skills, plus the\n * skills the end user added to their own instance\" entry point. A product holds\n * a canonical base `AgentProfile` (role/environment/tool-conventions rendered\n * into `prompt.systemPrompt`, baseline skills, baseline MCP). At deploy/turn\n * time it layers four file-mount channels onto `resources.files` —\n *\n * 1. skills — the always-mounted product skill corpus\n * 2. knowledge — a second always-mounted corpus (domain knowledge pack)\n * 3. registry — the tier-gated installable registry (free -> boot-mounted)\n * 4. userSkills — per-user / per-workspace skills the END USER adds to their\n * own instance, mounted at `~/.claude/skills/<id>/SKILL.md`\n * exactly like the registry's free tier\n *\n * plus an optional MCP overlay (delegation + per-turn app-tool side channel), a\n * per-turn `systemPrompt` override, and a `name` override. The merge is the SDK\n * `mergeAgentProfiles`: `mcp` is last-wins per key (base -> overlay), `resources`\n * arrays are concatenated (base ++ overlay), `prompt` is shallow-merged so an\n * overlay carrying only `systemPrompt` overrides it while keeping base\n * instructions. The compose algebra is DATA — the product injects the base\n * profile, the channel mounts (built with the `skills` subpath primitives), the\n * delegation/app-tool MCP map, and the override strings; nothing here reaches\n * for env, a glob, or a specific product's profile.\n *\n * The evolvable-section seam is the loader closure. A product's single\n * self-improvable domain section (the one `applyDomainPatch` targets) loads its\n * body from a deployed markdown override, falling back to an in-tree baseline.\n * The `import.meta.glob('<lit>', ...)` literal must stay at the CONSUMER call\n * site (Vite static-analyzes it), so `makeEvolvableSection` takes the loader as\n * a closure and a REQUIRED `baseline` — it never constructs a glob and never\n * defaults the baseline, so a product can't render an empty learned-guidance\n * section. `stripComments` is the shared \"is this addendum really empty?\" test.\n */\n\nimport type {\n AgentProfile,\n AgentProfileFileMount,\n AgentProfileMcpServer,\n} from '@tangle-network/sandbox'\nimport { mergeAgentProfiles } from '@tangle-network/sandbox'\nimport { profile } from '@tangle-network/agent-eval'\nimport {\n composeShellResources,\n registrySkills,\n skillMountPath,\n type ComposeShellResourcesInput,\n type SkillEntry,\n} from '../skills/index'\n\n/** Re-expose the agent-eval section/render substrate so a product wires the\n * evolvable surface through ONE subpath: `makeEvolvableSection` builds the\n * section, `profile.renderProfile` renders it, `profile.applyDomainPatch` lets\n * the loop patch it by id. The rendering/patching engine stays in agent-eval;\n * reach it through this namespace (re-exporting the bare fns would leak\n * agent-eval's un-nameable AgentProfile type into our generated d.ts). */\nexport { profile }\n\n/** The file-mount channels layered onto `resources.files`. The first three\n * mirror {@link ComposeShellResourcesInput}; `userSkills` is the per-user /\n * per-workspace channel — skills the END USER added to their own instance,\n * mounted at the harness skill-discovery path like the registry's free tier. */\nexport interface ProfileChannels {\n /** Always-mounted skill corpus (pass `corpusSkills(...)`). */\n skills?: AgentProfileFileMount[]\n /** Always-mounted knowledge corpus (pass `corpusSkills(...)` for the pack). */\n knowledge?: AgentProfileFileMount[]\n /** Single-file evolvable / learned-guidance corpora, if mounted as files. */\n evolvable?: AgentProfileFileMount[]\n /** Tier-gated installable registry (pass the registry array; free tier is\n * mounted, paid is install-on-demand). Gated through {@link registrySkills}. */\n registry?: SkillEntry[]\n /** Per-user / per-workspace skills the end user adds to their own instance.\n * Mounted at `~/.claude/skills/<id>/SKILL.md`, the same harness path the\n * registry uses, so a user skill and a registry skill with the same id\n * collide deterministically (the user skill, appended last, wins). */\n userSkills?: UserSkill[]\n /** Final skip filter applied to the composed mount list by mount `path`. */\n filesPredicate?: (mount: AgentProfileFileMount) => boolean\n}\n\n/** A per-user / per-workspace skill: an id and an inline `SKILL.md` body. The\n * user-facing analogue of a registry {@link SkillEntry} with no tier gate —\n * every user skill is mounted (the user opted in by adding it). */\nexport interface UserSkill {\n id: string\n /** Inline `SKILL.md` body mounted at {@link skillMountPath}. */\n skillMd: string\n}\n\n/** Overlay overrides applied on top of the channel mounts. */\nexport interface ProfileOverlay {\n /** Extra MCP servers merged into the profile `mcp` map (last-wins per key over\n * the base servers). The product builds this from its delegation MCP entry\n * and any per-turn app-tool side-channel servers. An absent/`undefined` entry\n * is dropped — pass only the servers that resolved (fail-closed at the seam,\n * not here). */\n mcp?: Record<string, AgentProfileMcpServer>\n /** Per-turn system-prompt override. When set, replaces the base\n * `prompt.systemPrompt` while keeping base `prompt.instructions`. When unset,\n * the base prompt passes through unchanged. */\n systemPrompt?: string\n /** Extra instruction lines merged onto the active prompt (e.g. a per-turn\n * domain/integration directive). Appended to base `prompt.instructions` by\n * the SDK merge. */\n instructions?: string[]\n /** Profile `name` override. When unset, the base name is kept. */\n name?: string\n}\n\n/** Project per-user skills onto SDK file mounts at the harness skill-discovery\n * path. No tier gate — a user skill is mounted because the user added it.\n * Sorted by path for determinism (matches {@link registrySkills}). */\nexport function userSkillMounts(userSkills: UserSkill[]): AgentProfileFileMount[] {\n return userSkills\n .map(\n (s) =>\n ({\n path: skillMountPath(s.id),\n resource: { kind: 'inline', name: s.id, content: s.skillMd },\n }) satisfies AgentProfileFileMount,\n )\n .sort((a, b) => a.path.localeCompare(b.path))\n}\n\n/**\n * Compose a deployable `AgentProfile` from a canonical base plus the four\n * file-mount channels and the overlay overrides.\n *\n * Files: base `resources.files` come first; the four channels follow in\n * `skills -> knowledge -> evolvable -> registry -> userSkills` order (so a\n * userSkill that mounts at the same path as a registry skill is the last write\n * and wins). MCP: base servers first, the overlay `mcp` last (last-wins per\n * key). Prompt: the overlay `systemPrompt`, when set, replaces the base one;\n * base instructions are preserved. Name: the overlay `name`, when set, wins.\n *\n * The merge delegates to the SDK `mergeAgentProfiles` (overlay-wins on records,\n * arrays concatenated) — the deterministic algebra is the overlay we hand it,\n * not a hand-rolled spread. `mergeAgentProfiles(base, overlay)` returns\n * `undefined` only when BOTH are `undefined`; `base` is always defined here, so\n * the result is non-`undefined` by construction and we assert that to the caller.\n */\nexport function composeAgentProfile(\n base: AgentProfile,\n channels: ProfileChannels = {},\n overlay: ProfileOverlay = {},\n): AgentProfile {\n const shellInput: ComposeShellResourcesInput = {\n skills: channels.skills,\n knowledge: channels.knowledge,\n evolvable: channels.evolvable,\n registry: channels.registry ? registrySkills(channels.registry) : undefined,\n predicate: channels.filesPredicate,\n }\n const channelFiles = composeShellResources(shellInput)\n const userFiles = channels.userSkills ? userSkillMounts(channels.userSkills) : []\n const overlayFiles = channels.filesPredicate\n ? userFiles.filter(channels.filesPredicate)\n : userFiles\n const files = [...channelFiles, ...overlayFiles]\n\n const promptOverlay: { systemPrompt?: string; instructions?: string[] } = {}\n if (overlay.systemPrompt) promptOverlay.systemPrompt = overlay.systemPrompt\n if (overlay.instructions && overlay.instructions.length > 0) promptOverlay.instructions = overlay.instructions\n\n const overlayProfile: AgentProfile = {\n ...(overlay.name ? { name: overlay.name } : {}),\n ...(Object.keys(promptOverlay).length > 0 ? { prompt: promptOverlay } : {}),\n ...(overlay.mcp ? { mcp: overlay.mcp } : {}),\n resources: { files },\n }\n\n const merged = mergeAgentProfiles(base, overlayProfile)\n if (!merged)\n throw new Error('composeAgentProfile: mergeAgentProfiles returned undefined for a defined base')\n return pruneEmptyResourceChannels(merged)\n}\n\n/** Drop empty resource channels the SDK merge normalizes in (`tools`/`skills`/\n * `agents`/`commands`: `[]`), so the composed profile's wire payload carries\n * only the channels that actually have content — one canonical shape every app\n * emits, instead of a sidecar payload full of empty arrays. */\nfunction pruneEmptyResourceChannels(profile: AgentProfile): AgentProfile {\n if (!profile.resources) return profile\n const kept = Object.fromEntries(\n Object.entries(profile.resources).filter(([, value]) => !(Array.isArray(value) && value.length === 0)),\n ) as AgentProfile['resources']\n const out: AgentProfile = { ...profile, resources: kept }\n if (kept && Object.keys(kept).length === 0) delete out.resources\n return out\n}\n\n/** True body of an addendum file with HTML comments stripped — an all-comment\n * placeholder counts as empty, so the loader falls back to the baseline. */\nexport function stripComments(raw: string): string {\n return raw.replace(/<!--[\\s\\S]*?-->/g, '').trim()\n}\n\n/** Inputs to {@link makeEvolvableSection}. */\nexport interface EvolvableSectionInput {\n /** Section id the self-improvement loop targets with `applyDomainPatch`. */\n id: string\n /** Section title rendered as `### <title>`. */\n title: string\n /**\n * Load the deployed section body. The CONSUMER supplies this closure and runs\n * its own `import.meta.glob('<lit>', { eager: true, query: '?raw', import:\n * 'default' })` inside it — the literal must stay at the call site so Vite can\n * static-analyze it; a glob constructed here would not resolve the product's\n * files. Return the raw markdown (comments and all); `makeEvolvableSection`\n * applies {@link stripComments} to decide whether it is really populated.\n */\n load: () => string\n /**\n * The in-tree fallback body, used when `load()` returns an\n * all-comments/empty placeholder. REQUIRED — no internal default — so a\n * product can never accidentally render an empty evolvable section.\n */\n baseline: string\n}\n\n/**\n * Build the one evolvable (`evolvable: true`) domain section whose body comes\n * from the product's loader, falling back to the required baseline when the\n * loaded body is empty after stripping comments. Returns the agent-eval\n * `AgentProfileSection` shape — drop it straight into `prodProfile`'s shipped\n * sections. The loader is the only seam; the empty-vs-populated rule and the\n * baseline fallback are the lifted algebra.\n */\nexport function makeEvolvableSection(input: EvolvableSectionInput): profile.AgentProfileSection {\n const loaded = input.load()\n const body = stripComments(loaded) ? loaded.trim() : input.baseline\n return { id: input.id, title: input.title, body, evolvable: true }\n}\n\nexport {\n composeShellResources,\n corpusSkills,\n loadMarkdownCorpus,\n registrySkills,\n skillMountPath,\n} from '../skills/index'\nexport type {\n ComposeShellResourcesInput,\n CorpusEntry,\n CorpusLoadResult,\n GlobModules,\n LoadCorpusOptions,\n SkillEntry,\n} from '../skills/index'\n"],"mappings":";;;;;;;;;AAyCA,SAAS,0BAA0B;AACnC,SAAS,eAAe;AAwEjB,SAAS,gBAAgB,YAAkD;AAChF,SAAO,WACJ;AAAA,IACC,CAAC,OACE;AAAA,MACC,MAAM,eAAe,EAAE,EAAE;AAAA,MACzB,UAAU,EAAE,MAAM,UAAU,MAAM,EAAE,IAAI,SAAS,EAAE,QAAQ;AAAA,IAC7D;AAAA,EACJ,EACC,KAAK,CAAC,GAAG,MAAM,EAAE,KAAK,cAAc,EAAE,IAAI,CAAC;AAChD;AAmBO,SAAS,oBACd,MACA,WAA4B,CAAC,GAC7B,UAA0B,CAAC,GACb;AACd,QAAM,aAAyC;AAAA,IAC7C,QAAQ,SAAS;AAAA,IACjB,WAAW,SAAS;AAAA,IACpB,WAAW,SAAS;AAAA,IACpB,UAAU,SAAS,WAAW,eAAe,SAAS,QAAQ,IAAI;AAAA,IAClE,WAAW,SAAS;AAAA,EACtB;AACA,QAAM,eAAe,sBAAsB,UAAU;AACrD,QAAM,YAAY,SAAS,aAAa,gBAAgB,SAAS,UAAU,IAAI,CAAC;AAChF,QAAM,eAAe,SAAS,iBAC1B,UAAU,OAAO,SAAS,cAAc,IACxC;AACJ,QAAM,QAAQ,CAAC,GAAG,cAAc,GAAG,YAAY;AAE/C,QAAM,gBAAoE,CAAC;AAC3E,MAAI,QAAQ,aAAc,eAAc,eAAe,QAAQ;AAC/D,MAAI,QAAQ,gBAAgB,QAAQ,aAAa,SAAS,EAAG,eAAc,eAAe,QAAQ;AAElG,QAAM,iBAA+B;AAAA,IACnC,GAAI,QAAQ,OAAO,EAAE,MAAM,QAAQ,KAAK,IAAI,CAAC;AAAA,IAC7C,GAAI,OAAO,KAAK,aAAa,EAAE,SAAS,IAAI,EAAE,QAAQ,cAAc,IAAI,CAAC;AAAA,IACzE,GAAI,QAAQ,MAAM,EAAE,KAAK,QAAQ,IAAI,IAAI,CAAC;AAAA,IAC1C,WAAW,EAAE,MAAM;AAAA,EACrB;AAEA,QAAM,SAAS,mBAAmB,MAAM,cAAc;AACtD,MAAI,CAAC;AACH,UAAM,IAAI,MAAM,+EAA+E;AACjG,SAAO,2BAA2B,MAAM;AAC1C;AAMA,SAAS,2BAA2BA,UAAqC;AACvE,MAAI,CAACA,SAAQ,UAAW,QAAOA;AAC/B,QAAM,OAAO,OAAO;AAAA,IAClB,OAAO,QAAQA,SAAQ,SAAS,EAAE,OAAO,CAAC,CAAC,EAAE,KAAK,MAAM,EAAE,MAAM,QAAQ,KAAK,KAAK,MAAM,WAAW,EAAE;AAAA,EACvG;AACA,QAAM,MAAoB,EAAE,GAAGA,UAAS,WAAW,KAAK;AACxD,MAAI,QAAQ,OAAO,KAAK,IAAI,EAAE,WAAW,EAAG,QAAO,IAAI;AACvD,SAAO;AACT;AAIO,SAAS,cAAc,KAAqB;AACjD,SAAO,IAAI,QAAQ,oBAAoB,EAAE,EAAE,KAAK;AAClD;AAiCO,SAAS,qBAAqB,OAA2D;AAC9F,QAAM,SAAS,MAAM,KAAK;AAC1B,QAAM,OAAO,cAAc,MAAM,IAAI,OAAO,KAAK,IAAI,MAAM;AAC3D,SAAO,EAAE,IAAI,MAAM,IAAI,OAAO,MAAM,OAAO,MAAM,WAAW,KAAK;AACnE;","names":["profile"]}
@@ -0,0 +1,77 @@
1
+ /**
2
+ * System-prompt assembler for agent products.
3
+ *
4
+ * Every agent product composes its system prompt the same way: a base profile
5
+ * prompt (the operator persona + tools block + any skills section the consumer
6
+ * already rendered), an always-on operating directive, then an ordered list of
7
+ * optional per-turn context sections (known-context, board, approval history,
8
+ * learned-style, pending questions, the active artifact). The ORDERING and the
9
+ * whitespace contract are identical across products; only the section BODIES are
10
+ * domain. This module owns the ordering and the joins; the product injects every
11
+ * body as an already-rendered string through the `sections` array.
12
+ *
13
+ * Whitespace contract (preserves the join/concat/trim algebra of the per-product
14
+ * pre-lift composers — a product adopting this asserts byte-for-byte parity in
15
+ * its own suite):
16
+ * - base is emitted verbatim, with no leading/trailing normalization.
17
+ * - directive is appended after base with a single `\n\n` join.
18
+ * - each section in `sections` is appended with NO separator: a section is
19
+ * either '' (absent — contributes nothing) or already carries its own
20
+ * leading `\n\n` (and its `## ` heading). The assembler concatenates them
21
+ * unconditionally, which is why the conditional-prefix-or-empty contract
22
+ * lives in the product's section renderers, not here.
23
+ * - `trim` (default false) applies a final `.trim()` to the whole result.
24
+ * Branches that historically trimmed pass `trim: true`; branches that did
25
+ * not (e.g. the new-workspace paths) leave it false, preserving that
26
+ * asymmetry rather than silently changing trailing whitespace.
27
+ *
28
+ * Pure string composition: no SDK runtime symbol, no node builtins, no glob.
29
+ * The base profile prompt and the skills-section insertion point both live
30
+ * INSIDE the product-built `base` string — the assembler never reaches into an
31
+ * AgentProfile and never loads a corpus. The sibling
32
+ * `@tangle-network/agent-app/skills` subpath loads the corpus and returns file
33
+ * mounts (`AgentProfileFileMount[]`); the PRODUCT renders any `## Skills` text
34
+ * section and folds it into `base` before calling this assembler. This module
35
+ * never renders a skills heading.
36
+ */
37
+ /** Inputs to {@link assembleSystemPrompt}. */
38
+ interface AssembleSystemPromptInput {
39
+ /** The product's already-composed base block: persona/system prompt + tools
40
+ * block + (optionally) the rendered skills section. The product is
41
+ * responsible for resolving its profile system prompt and failing loud if it
42
+ * is absent — an empty base reaches this assembler only as a programmer
43
+ * error, which it rejects (see {@link AssembleResult}). */
44
+ base: string;
45
+ /** The always-on operating directive, placed immediately after base with a
46
+ * `\n\n` join. Pass '' to omit it (the join is then suppressed). */
47
+ directive?: string;
48
+ /** Ordered per-turn context sections, each already rendered by the product to
49
+ * either '' (absent) or a `\n\n## …`-prefixed string. Concatenated in order
50
+ * with no added separator. */
51
+ sections?: string[];
52
+ /** Apply a final `.trim()` to the composed result. Default false — set true
53
+ * only on branches whose pre-lift output was trimmed. */
54
+ trim?: boolean;
55
+ }
56
+ /** Typed outcome of {@link assembleSystemPrompt}. `succeeded: false` is returned
57
+ * for a programmer error (an empty base) rather than emitting a roleless
58
+ * prompt — callers MUST inspect `succeeded` before using `prompt`. */
59
+ type AssembleResult = {
60
+ succeeded: true;
61
+ prompt: string;
62
+ } | {
63
+ succeeded: false;
64
+ error: string;
65
+ };
66
+ /**
67
+ * Assemble a system prompt from a base block, an operating directive, and an
68
+ * ordered list of pre-rendered context sections.
69
+ *
70
+ * Returns a typed outcome: an empty/blank `base` is a defect (an agent with no
71
+ * persona/system prompt), so it fails loud instead of silently producing a
72
+ * prompt with no role. The product resolves and validates its profile prompt
73
+ * upstream; this is the last-line guard at the seam.
74
+ */
75
+ declare function assembleSystemPrompt(input: AssembleSystemPromptInput): AssembleResult;
76
+
77
+ export { type AssembleResult, type AssembleSystemPromptInput, assembleSystemPrompt };
@@ -0,0 +1,22 @@
1
+ // src/prompt/index.ts
2
+ function isBlank(value) {
3
+ return value.trim().length === 0;
4
+ }
5
+ function assembleSystemPrompt(input) {
6
+ const { base, directive = "", sections = [], trim = false } = input;
7
+ if (isBlank(base)) {
8
+ return {
9
+ succeeded: false,
10
+ error: "assembleSystemPrompt: base is empty \u2014 a system prompt with no persona/base block is a defect, not a default"
11
+ };
12
+ }
13
+ let prompt = directive.length > 0 ? `${base}
14
+
15
+ ${directive}` : base;
16
+ for (const section of sections) prompt += section;
17
+ return { succeeded: true, prompt: trim ? prompt.trim() : prompt };
18
+ }
19
+ export {
20
+ assembleSystemPrompt
21
+ };
22
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/prompt/index.ts"],"sourcesContent":["/**\n * System-prompt assembler for agent products.\n *\n * Every agent product composes its system prompt the same way: a base profile\n * prompt (the operator persona + tools block + any skills section the consumer\n * already rendered), an always-on operating directive, then an ordered list of\n * optional per-turn context sections (known-context, board, approval history,\n * learned-style, pending questions, the active artifact). The ORDERING and the\n * whitespace contract are identical across products; only the section BODIES are\n * domain. This module owns the ordering and the joins; the product injects every\n * body as an already-rendered string through the `sections` array.\n *\n * Whitespace contract (preserves the join/concat/trim algebra of the per-product\n * pre-lift composers — a product adopting this asserts byte-for-byte parity in\n * its own suite):\n * - base is emitted verbatim, with no leading/trailing normalization.\n * - directive is appended after base with a single `\\n\\n` join.\n * - each section in `sections` is appended with NO separator: a section is\n * either '' (absent — contributes nothing) or already carries its own\n * leading `\\n\\n` (and its `## ` heading). The assembler concatenates them\n * unconditionally, which is why the conditional-prefix-or-empty contract\n * lives in the product's section renderers, not here.\n * - `trim` (default false) applies a final `.trim()` to the whole result.\n * Branches that historically trimmed pass `trim: true`; branches that did\n * not (e.g. the new-workspace paths) leave it false, preserving that\n * asymmetry rather than silently changing trailing whitespace.\n *\n * Pure string composition: no SDK runtime symbol, no node builtins, no glob.\n * The base profile prompt and the skills-section insertion point both live\n * INSIDE the product-built `base` string — the assembler never reaches into an\n * AgentProfile and never loads a corpus. The sibling\n * `@tangle-network/agent-app/skills` subpath loads the corpus and returns file\n * mounts (`AgentProfileFileMount[]`); the PRODUCT renders any `## Skills` text\n * section and folds it into `base` before calling this assembler. This module\n * never renders a skills heading.\n */\n\n/** Inputs to {@link assembleSystemPrompt}. */\nexport interface AssembleSystemPromptInput {\n /** The product's already-composed base block: persona/system prompt + tools\n * block + (optionally) the rendered skills section. The product is\n * responsible for resolving its profile system prompt and failing loud if it\n * is absent — an empty base reaches this assembler only as a programmer\n * error, which it rejects (see {@link AssembleResult}). */\n base: string\n /** The always-on operating directive, placed immediately after base with a\n * `\\n\\n` join. Pass '' to omit it (the join is then suppressed). */\n directive?: string\n /** Ordered per-turn context sections, each already rendered by the product to\n * either '' (absent) or a `\\n\\n## …`-prefixed string. Concatenated in order\n * with no added separator. */\n sections?: string[]\n /** Apply a final `.trim()` to the composed result. Default false — set true\n * only on branches whose pre-lift output was trimmed. */\n trim?: boolean\n}\n\n/** Typed outcome of {@link assembleSystemPrompt}. `succeeded: false` is returned\n * for a programmer error (an empty base) rather than emitting a roleless\n * prompt — callers MUST inspect `succeeded` before using `prompt`. */\nexport type AssembleResult =\n | { succeeded: true; prompt: string }\n | { succeeded: false; error: string }\n\n/** True when a string is empty or whitespace-only. */\nfunction isBlank(value: string): boolean {\n return value.trim().length === 0\n}\n\n/**\n * Assemble a system prompt from a base block, an operating directive, and an\n * ordered list of pre-rendered context sections.\n *\n * Returns a typed outcome: an empty/blank `base` is a defect (an agent with no\n * persona/system prompt), so it fails loud instead of silently producing a\n * prompt with no role. The product resolves and validates its profile prompt\n * upstream; this is the last-line guard at the seam.\n */\nexport function assembleSystemPrompt(input: AssembleSystemPromptInput): AssembleResult {\n const { base, directive = '', sections = [], trim = false } = input\n\n if (isBlank(base)) {\n return {\n succeeded: false,\n error: 'assembleSystemPrompt: base is empty — a system prompt with no persona/base block is a defect, not a default',\n }\n }\n\n let prompt = directive.length > 0 ? `${base}\\n\\n${directive}` : base\n for (const section of sections) prompt += section\n\n return { succeeded: true, prompt: trim ? prompt.trim() : prompt }\n}"],"mappings":";AAiEA,SAAS,QAAQ,OAAwB;AACvC,SAAO,MAAM,KAAK,EAAE,WAAW;AACjC;AAWO,SAAS,qBAAqB,OAAkD;AACrF,QAAM,EAAE,MAAM,YAAY,IAAI,WAAW,CAAC,GAAG,OAAO,MAAM,IAAI;AAE9D,MAAI,QAAQ,IAAI,GAAG;AACjB,WAAO;AAAA,MACL,WAAW;AAAA,MACX,OAAO;AAAA,IACT;AAAA,EACF;AAEA,MAAI,SAAS,UAAU,SAAS,IAAI,GAAG,IAAI;AAAA;AAAA,EAAO,SAAS,KAAK;AAChE,aAAW,WAAW,SAAU,WAAU;AAE1C,SAAO,EAAE,WAAW,MAAM,QAAQ,OAAO,OAAO,KAAK,IAAI,OAAO;AAClE;","names":[]}
@@ -0,0 +1,64 @@
1
+ import { Harness } from '../harness/index.js';
2
+
3
+ /**
4
+ * Execution-mode dispatch — the shell entry that infers *how* to run an agent
5
+ * from its profile's harness, so a product hands over one profile and the
6
+ * framework routes to the router tool loop or a sandbox without separate wiring.
7
+ *
8
+ * The discriminator is the harness: absent / null / 'router' => the router tool
9
+ * agent (`@tangle-network/agent-app/runtime`); any member of `KNOWN_HARNESSES`
10
+ * (opencode, claude-code, …) => a sandbox (`@tangle-network/agent-app/sandbox`).
11
+ * `KNOWN_HARNESSES` is exactly the set of sandbox backends; the *absence* of a
12
+ * harness is the router signal.
13
+ *
14
+ * `runAgent` owns the inference + routing; the two branch runners are injected,
15
+ * because their inputs and event shapes legitimately differ (the router loop
16
+ * yields `LoopEvent`s, the sandbox yields its own events). A product supplies a
17
+ * `router`/`sandbox` thunk and can lazy-import the sandbox driver inside its own
18
+ * thunk, so a router-only app never resolves `@tangle-network/sandbox`.
19
+ */
20
+
21
+ type ExecutionMode = 'router' | 'sandbox';
22
+ /** The router pseudo-harness — the absence of a sandbox backend. Not a member
23
+ * of `KNOWN_HARNESSES`; callers may pass it explicitly instead of null. */
24
+ declare const ROUTER_HARNESS: "router";
25
+ type RouterHarness = typeof ROUTER_HARNESS;
26
+ type ProfileHarness = Harness | RouterHarness | null | undefined;
27
+ /**
28
+ * Infer the execution mode from a profile's harness. Absent / null / 'router'
29
+ * => 'router'; a known sandbox harness => 'sandbox'. An unrecognized non-null
30
+ * string is treated as a sandbox backend (the box installs all harnesses, so an
31
+ * unknown id is a sandbox the runtime resolves, not a router) — callers that
32
+ * want strictness can validate against `KNOWN_HARNESSES` first.
33
+ */
34
+ declare function resolveExecutionMode(harness: ProfileHarness): ExecutionMode;
35
+ /** True when `harness` names a sandbox backend the SDK knows about. */
36
+ declare function isKnownSandboxHarness(harness: ProfileHarness): harness is Harness;
37
+ /**
38
+ * The superset the dispatch reads: the SDK `AgentProfile` plus the harness
39
+ * discriminator. Intentionally pins only `harness` — the rest of the profile is
40
+ * carried opaquely so this module stays free of the SDK type (router-only apps
41
+ * import it without `@tangle-network/sandbox`).
42
+ */
43
+ interface ShellProfile {
44
+ harness?: ProfileHarness;
45
+ }
46
+ /** Execution mode for a profile — `harness` omitted/null => router. */
47
+ declare function executionModeForProfile(profile: ShellProfile): ExecutionMode;
48
+ /** The two branch runners. Each returns (or resolves to) an async iterable of
49
+ * the product's own event type — the shapes differ, so the product owns them.
50
+ * A `sandbox` thunk can `await import('@tangle-network/agent-app/sandbox')`
51
+ * internally so the SDK is pulled only on the sandbox path. */
52
+ interface RunAgentBranches<T> {
53
+ router: () => AsyncIterable<T> | Promise<AsyncIterable<T>>;
54
+ sandbox: () => AsyncIterable<T> | Promise<AsyncIterable<T>>;
55
+ }
56
+ /**
57
+ * Route a turn to the branch the harness selects and stream its events through.
58
+ * The shell does the inference; the product supplies how each branch runs. The
59
+ * unselected branch's thunk is never invoked — so its dependencies (e.g. the
60
+ * sandbox SDK) are never loaded on the other path.
61
+ */
62
+ declare function runAgent<T>(harness: ProfileHarness, branches: RunAgentBranches<T>): AsyncGenerator<T>;
63
+
64
+ export { type ExecutionMode, type ProfileHarness, ROUTER_HARNESS, type RouterHarness, type RunAgentBranches, type ShellProfile, executionModeForProfile, isKnownSandboxHarness, resolveExecutionMode, runAgent };
@@ -0,0 +1,30 @@
1
+ import {
2
+ KNOWN_HARNESSES
3
+ } from "../chunk-SD2H4FWY.js";
4
+
5
+ // src/run/index.ts
6
+ var ROUTER_HARNESS = "router";
7
+ function resolveExecutionMode(harness) {
8
+ if (harness == null || harness === ROUTER_HARNESS) return "router";
9
+ return "sandbox";
10
+ }
11
+ function isKnownSandboxHarness(harness) {
12
+ return harness != null && harness !== ROUTER_HARNESS && KNOWN_HARNESSES.includes(harness);
13
+ }
14
+ function executionModeForProfile(profile) {
15
+ return resolveExecutionMode(profile.harness);
16
+ }
17
+ async function* runAgent(harness, branches) {
18
+ const mode = resolveExecutionMode(harness);
19
+ const source = mode === "sandbox" ? branches.sandbox() : branches.router();
20
+ const iterable = await source;
21
+ for await (const event of iterable) yield event;
22
+ }
23
+ export {
24
+ ROUTER_HARNESS,
25
+ executionModeForProfile,
26
+ isKnownSandboxHarness,
27
+ resolveExecutionMode,
28
+ runAgent
29
+ };
30
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/run/index.ts"],"sourcesContent":["/**\n * Execution-mode dispatch — the shell entry that infers *how* to run an agent\n * from its profile's harness, so a product hands over one profile and the\n * framework routes to the router tool loop or a sandbox without separate wiring.\n *\n * The discriminator is the harness: absent / null / 'router' => the router tool\n * agent (`@tangle-network/agent-app/runtime`); any member of `KNOWN_HARNESSES`\n * (opencode, claude-code, …) => a sandbox (`@tangle-network/agent-app/sandbox`).\n * `KNOWN_HARNESSES` is exactly the set of sandbox backends; the *absence* of a\n * harness is the router signal.\n *\n * `runAgent` owns the inference + routing; the two branch runners are injected,\n * because their inputs and event shapes legitimately differ (the router loop\n * yields `LoopEvent`s, the sandbox yields its own events). A product supplies a\n * `router`/`sandbox` thunk and can lazy-import the sandbox driver inside its own\n * thunk, so a router-only app never resolves `@tangle-network/sandbox`.\n */\n\nimport { KNOWN_HARNESSES, type Harness } from '../harness/index'\n\nexport type ExecutionMode = 'router' | 'sandbox'\n\n/** The router pseudo-harness — the absence of a sandbox backend. Not a member\n * of `KNOWN_HARNESSES`; callers may pass it explicitly instead of null. */\nexport const ROUTER_HARNESS = 'router' as const\nexport type RouterHarness = typeof ROUTER_HARNESS\n\nexport type ProfileHarness = Harness | RouterHarness | null | undefined\n\n/**\n * Infer the execution mode from a profile's harness. Absent / null / 'router'\n * => 'router'; a known sandbox harness => 'sandbox'. An unrecognized non-null\n * string is treated as a sandbox backend (the box installs all harnesses, so an\n * unknown id is a sandbox the runtime resolves, not a router) — callers that\n * want strictness can validate against `KNOWN_HARNESSES` first.\n */\nexport function resolveExecutionMode(harness: ProfileHarness): ExecutionMode {\n if (harness == null || harness === ROUTER_HARNESS) return 'router'\n return 'sandbox'\n}\n\n/** True when `harness` names a sandbox backend the SDK knows about. */\nexport function isKnownSandboxHarness(harness: ProfileHarness): harness is Harness {\n return harness != null && harness !== ROUTER_HARNESS && (KNOWN_HARNESSES as readonly string[]).includes(harness)\n}\n\n/**\n * The superset the dispatch reads: the SDK `AgentProfile` plus the harness\n * discriminator. Intentionally pins only `harness` — the rest of the profile is\n * carried opaquely so this module stays free of the SDK type (router-only apps\n * import it without `@tangle-network/sandbox`).\n */\nexport interface ShellProfile {\n harness?: ProfileHarness\n}\n\n/** Execution mode for a profile — `harness` omitted/null => router. */\nexport function executionModeForProfile(profile: ShellProfile): ExecutionMode {\n return resolveExecutionMode(profile.harness)\n}\n\n/** The two branch runners. Each returns (or resolves to) an async iterable of\n * the product's own event type — the shapes differ, so the product owns them.\n * A `sandbox` thunk can `await import('@tangle-network/agent-app/sandbox')`\n * internally so the SDK is pulled only on the sandbox path. */\nexport interface RunAgentBranches<T> {\n router: () => AsyncIterable<T> | Promise<AsyncIterable<T>>\n sandbox: () => AsyncIterable<T> | Promise<AsyncIterable<T>>\n}\n\n/**\n * Route a turn to the branch the harness selects and stream its events through.\n * The shell does the inference; the product supplies how each branch runs. The\n * unselected branch's thunk is never invoked — so its dependencies (e.g. the\n * sandbox SDK) are never loaded on the other path.\n */\nexport async function* runAgent<T>(\n harness: ProfileHarness,\n branches: RunAgentBranches<T>,\n): AsyncGenerator<T> {\n const mode = resolveExecutionMode(harness)\n const source = mode === 'sandbox' ? branches.sandbox() : branches.router()\n const iterable = await source\n for await (const event of iterable) yield event\n}\n"],"mappings":";;;;;AAwBO,IAAM,iBAAiB;AAYvB,SAAS,qBAAqB,SAAwC;AAC3E,MAAI,WAAW,QAAQ,YAAY,eAAgB,QAAO;AAC1D,SAAO;AACT;AAGO,SAAS,sBAAsB,SAA6C;AACjF,SAAO,WAAW,QAAQ,YAAY,kBAAmB,gBAAsC,SAAS,OAAO;AACjH;AAaO,SAAS,wBAAwB,SAAsC;AAC5E,SAAO,qBAAqB,QAAQ,OAAO;AAC7C;AAiBA,gBAAuB,SACrB,SACA,UACmB;AACnB,QAAM,OAAO,qBAAqB,OAAO;AACzC,QAAM,SAAS,SAAS,YAAY,SAAS,QAAQ,IAAI,SAAS,OAAO;AACzE,QAAM,WAAW,MAAM;AACvB,mBAAiB,SAAS,SAAU,OAAM;AAC5C;","names":[]}
@@ -5,7 +5,8 @@ export { C as CatalogModel, M as ModelCatalog, R as RouterModel, _ as __resetCat
5
5
  export { C as CreateTangleRouterModelConfigOptions, D as DEFAULT_TANGLE_BILLING_ENFORCEMENT_ENV_VAR, a as DEFAULT_TANGLE_ROUTER_BASE_URL, R as ResolveModelOptions, b as ResolveUserTangleExecutionKeyForUserOptions, c as ResolveUserTangleExecutionKeyOptions, d as ResolvedTangleExecutionKey, T as TangleBillingEnforcementOptions, e as TangleExecutionEnvironment, f as TangleExecutionKeyError, g as TangleExecutionKeyErrorCode, h as TangleExecutionKeyHttpError, i as TangleExecutionKeySource, j as TangleModelConfig, k as createTangleRouterModelConfig, l as isTangleBillingEnforcementDisabled, m as isTangleExecutionKeyError, r as resolveTangleExecutionEnvironment, n as resolveTangleModelConfig, o as resolveUserTangleExecutionKey, p as resolveUserTangleExecutionKeyForUser, t as tangleExecutionKeyHttpError } from '../model-CKzniMMr.js';
6
6
  import { b as AppToolContext, e as AppToolProducedEvent, f as AppToolTaxonomy, c as AppToolHandlers, d as AppToolOutcome } from '../types-2rOJo8Hc.js';
7
7
  import { CertifiedProfile } from '@tangle-network/agent-runtime/intelligence';
8
- import { a as AppToolMcpServer } from '../mcp-eZCmkgCF.js';
8
+ import { A as AppToolMcpServer } from '../mcp-BShTlESm.js';
9
+ import '../auth-BlS9GWfL.js';
9
10
 
10
11
  /**
11
12
  * OpenAI-compatible stream → `LoopEvent` adapter, for NON-sandbox copilots.