@tangle-network/agent-app 0.19.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.
@@ -1,21 +1,20 @@
1
1
  /**
2
- * Chat-time model resolution: a precedence resolver and a fail-closed catalog
3
- * validator that sit on top of a product's boot-time model config.
2
+ * Canonical chat-model resolution identical across every agent app.
4
3
  *
5
- * `resolveChatModel` picks the model id for a chat turn by precedence:
6
- * request id > env MODEL_NAME > provider default > sandbox default.
4
+ * The ONLY per-app inputs are DATA, never logic: the default model, the
5
+ * allowlist, the env value the deployment set, and the catalog-fetch loader.
6
+ * The logic is one precedence ladder + one fail-closed validator that every
7
+ * product uses the same way — there is no per-product variant, no env-var name
8
+ * baked in, and no backend dimension (router-vs-sandbox is the harness/dispatch
9
+ * concern, not model resolution; a sandbox's provider default lives in the
10
+ * sandbox subpath).
7
11
  *
8
- * `validateChatModelId` is the fail-closed gate: it returns a typed outcome and
9
- * accepts an id only if it is in the constructed allowlist OR served by the live
10
- * router catalog (loaded through an injected boundary). A bare id with no
11
- * provider prefix resolves to its canonical id only when the suffix is unique
12
- * across the catalog, so an ambiguous suffix is rejected rather than silently
13
- * assigned a provider.
14
- *
15
- * The product injects one value — `modelDefaults` — and supplies the catalog
16
- * loader per call. `ModelInfo` is the router /v1/models wire shape and
17
- * `canonicalModelId` the bare->prefixed id helper, both defined locally so this
18
- * engine module carries no UI-package coupling.
12
+ * - resolveChatModel: request > workspace > env > default. The product reads its
13
+ * own deploy env var and passes the VALUE as `envModel`; the shell knows no
14
+ * env-var names. Source is canonical: 'request' | 'workspace' | 'env' | 'default'.
15
+ * - validateChatModelId: fail-closed. Admit an id that is in the allowlist, or
16
+ * equals the operator-set env model, or is served by the live router catalog
17
+ * (exact, or a bare id resolved to its canonical id when the suffix is unique).
19
18
  */
20
19
  /** The router /v1/models entry shape this module reads. Minimal on purpose. */
21
20
  interface ModelInfo {
@@ -24,12 +23,9 @@ interface ModelInfo {
24
23
  _provider?: string;
25
24
  provider?: string;
26
25
  }
27
- /** Which execution path the chat turn runs on. Product-supplied per turn. */
28
- type ChatBackend = 'router' | 'sandbox';
29
- type ChatModelSource = 'request' | 'env:MODEL_NAME' | 'default' | 'sandbox-default';
26
+ type ChatModelSource = 'request' | 'workspace' | 'env' | 'default';
30
27
  interface ResolvedChatModel {
31
- backend: ChatBackend;
32
- model?: string;
28
+ model: string;
33
29
  source: ChatModelSource;
34
30
  }
35
31
  interface ChatModelValidationSuccess {
@@ -43,41 +39,40 @@ interface ChatModelValidationFailure {
43
39
  type ChatModelValidationResult = ChatModelValidationSuccess | ChatModelValidationFailure;
44
40
  /** The catalog-fetch boundary: maps a router base URL to the raw model list. */
45
41
  type LoadModels = (routerBaseUrl: string) => Promise<ModelInfo[]>;
46
- /**
47
- * The single product-injected seam.
48
- *
49
- * - `routerModel` / `sandboxOpenaiModel`: the two `DEFAULT_*` ids used by the
50
- * precedence ladder and seeded into the allowlist.
51
- * - `routerBaseUrl`: catalog endpoint base; overridable per validate call.
52
- * - `extraAllowlist`: additional ids accepted without a catalog round-trip.
53
- */
54
- interface ChatModelDefaults {
55
- routerModel: string;
56
- sandboxOpenaiModel: string;
57
- routerBaseUrl?: string;
58
- extraAllowlist?: string[];
42
+ interface ResolveChatModelInput {
43
+ /** Per-request override (highest precedence). */
44
+ requestModel?: string;
45
+ /** Persisted workspace-pinned model. */
46
+ workspaceModel?: string;
47
+ /** The value the deployment's model env var holds (the product reads its own
48
+ * var name and passes the value the shell stays env-var-name agnostic). */
49
+ envModel?: string;
50
+ /** Final fallback (the product's default, typically profile.model.default). */
51
+ defaultModel: string;
59
52
  }
60
- interface ResolveChatModelOptions {
61
- requestedModel?: string;
62
- backend: ChatBackend;
63
- /** Env to read (defaults to process.env). Inject for non-node runtimes. */
64
- env?: Record<string, string | undefined>;
65
- }
66
- interface ValidateChatModelIdOptions {
53
+ /** Resolve the chat-turn model by the one canonical precedence. Blank values are
54
+ * treated as absent. */
55
+ declare function resolveChatModel(input: ResolveChatModelInput): ResolvedChatModel;
56
+ interface ValidateChatModelIdInput {
57
+ /** Ids accepted without a catalog round-trip (defaults + operator-trusted). */
58
+ allowlist?: Iterable<string>;
59
+ /** The operator-set env model value — always admitted (operator-trusted). */
60
+ envModel?: string;
61
+ /** Catalog loader; required to reach the catalog path. */
62
+ loadModels?: LoadModels;
63
+ /** Catalog endpoint base; required to reach the catalog path. */
67
64
  routerBaseUrl?: string;
68
- /** Catalog loader. No default body is baked in; the consumer supplies it. */
69
- loadModels: LoadModels;
70
- }
71
- interface ChatModelResolution {
72
- resolveChatModel: (options: ResolveChatModelOptions) => ResolvedChatModel;
73
- validateChatModelId: (modelId: unknown, options: ValidateChatModelIdOptions) => Promise<ChatModelValidationResult>;
74
- DEFAULT_ROUTER_MODEL: string;
75
- DEFAULT_SANDBOX_OPENAI_MODEL: string;
76
- DEFAULT_ROUTER_BASE_URL?: string;
77
65
  }
78
- declare function createChatModelResolution(defaults: ChatModelDefaults): ChatModelResolution;
66
+ /**
67
+ * Fail-closed model-id validation. Accepts an id only when it is well-formed AND
68
+ * (in the allowlist, or equals the operator-set env model, or served by the live
69
+ * catalog). A bare id (no provider prefix) resolves to its canonical id only when
70
+ * the suffix is unique across the catalog — an ambiguous suffix is rejected
71
+ * rather than silently assigned a provider.
72
+ */
73
+ declare function validateChatModelId(modelId: unknown, input: ValidateChatModelIdInput): Promise<ChatModelValidationResult>;
79
74
  declare function cleanModelId(value: unknown): string | undefined;
80
75
  declare function isWellFormedModelId(modelId: string): boolean;
81
76
  declare function catalogIdsForModel(model: ModelInfo): string[];
82
77
 
83
- export { type ChatBackend, type ChatModelDefaults, type ChatModelResolution, type ChatModelSource, type ChatModelValidationFailure, type ChatModelValidationResult, type ChatModelValidationSuccess, type LoadModels, type ModelInfo, type ResolveChatModelOptions, type ResolvedChatModel, type ValidateChatModelIdOptions, catalogIdsForModel, cleanModelId, createChatModelResolution, isWellFormedModelId };
78
+ export { type ChatModelSource, type ChatModelValidationFailure, type ChatModelValidationResult, type ChatModelValidationSuccess, type LoadModels, type ModelInfo, type ResolveChatModelInput, type ResolvedChatModel, type ValidateChatModelIdInput, catalogIdsForModel, cleanModelId, isWellFormedModelId, resolveChatModel, validateChatModelId };
@@ -4,93 +4,49 @@ function canonicalModelId(model) {
4
4
  const provider = model._provider ?? model.provider;
5
5
  return provider ? `${provider}/${model.id}` : model.id;
6
6
  }
7
- function createChatModelResolution(defaults) {
8
- const DEFAULT_ROUTER_MODEL = defaults.routerModel;
9
- const DEFAULT_SANDBOX_OPENAI_MODEL = defaults.sandboxOpenaiModel;
10
- const DEFAULT_ROUTER_BASE_URL = defaults.routerBaseUrl;
11
- const allowlist = new Set(
12
- [
13
- DEFAULT_ROUTER_MODEL,
14
- DEFAULT_SANDBOX_OPENAI_MODEL,
15
- ...defaults.extraAllowlist ?? []
16
- ].filter((model) => typeof model === "string" && model.length > 0)
17
- );
18
- function resolveChatModel({
19
- requestedModel,
20
- backend,
21
- env = process.env
22
- }) {
23
- const selectedModel = cleanModelId(requestedModel);
24
- if (selectedModel) return { backend, model: selectedModel, source: "request" };
25
- if (backend === "router") {
26
- const routerModel = cleanModelId(env.MODEL_NAME);
27
- return {
28
- backend,
29
- model: routerModel ?? DEFAULT_ROUTER_MODEL,
30
- source: routerModel ? "env:MODEL_NAME" : "default"
31
- };
32
- }
33
- const sandboxModel = cleanModelId(env.MODEL_NAME);
34
- if (sandboxModel) return { backend, model: sandboxModel, source: "env:MODEL_NAME" };
35
- const modelProvider = env.MODEL_PROVIDER ?? (env.TANGLE_API_KEY ? "openai-compat" : env.OPENAI_API_KEY ? "openai" : void 0);
36
- if (modelProvider === "openai" || modelProvider === "openai-compat") {
37
- return { backend, model: DEFAULT_SANDBOX_OPENAI_MODEL, source: "default" };
38
- }
39
- return { backend, source: "sandbox-default" };
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}` };
40
25
  }
41
- async function validateChatModelId(modelId, {
42
- routerBaseUrl = DEFAULT_ROUTER_BASE_URL,
43
- loadModels
44
- }) {
45
- const cleaned = cleanModelId(modelId);
46
- if (!cleaned) {
47
- return { succeeded: false, error: "Model id must be a non-empty string." };
48
- }
49
- if (!isWellFormedModelId(cleaned)) {
50
- return { succeeded: false, error: `Model id is malformed: ${cleaned}` };
51
- }
52
- if (allowlist.has(cleaned)) {
53
- return { succeeded: true, value: cleaned };
54
- }
55
- if (typeof routerBaseUrl !== "string" || routerBaseUrl.length === 0) {
56
- return { succeeded: false, error: "Router base URL is required to validate against the catalog." };
57
- }
58
- let catalog;
59
- try {
60
- catalog = await loadModels(routerBaseUrl);
61
- } catch (err) {
62
- const message = err instanceof Error ? err.message : String(err);
63
- return { succeeded: false, error: `Could not validate model catalog: ${message}` };
64
- }
65
- const ids = new Set(catalog.flatMap(catalogIdsForModel));
66
- if (ids.has(cleaned)) {
67
- return { succeeded: true, value: cleaned };
68
- }
69
- if (!cleaned.includes("/")) {
70
- const canonicalBySuffix = /* @__PURE__ */ new Map();
71
- for (const model of catalog) {
72
- const canonical = canonicalModelIdOrUndefined(model);
73
- if (!canonical || !canonical.includes("/")) continue;
74
- const suffix = canonical.split("/").slice(1).join("/");
75
- const entries = canonicalBySuffix.get(suffix);
76
- if (entries) entries.push(canonical);
77
- else canonicalBySuffix.set(suffix, [canonical]);
78
- }
79
- const matches = canonicalBySuffix.get(cleaned);
80
- const only = matches && matches.length === 1 ? matches[0] : void 0;
81
- if (only) {
82
- return { succeeded: true, value: only };
83
- }
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]);
84
45
  }
85
- return { succeeded: false, error: `Model is not available: ${cleaned}` };
46
+ const matches = canonicalBySuffix.get(cleaned);
47
+ if (matches && matches.length === 1) return { succeeded: true, value: matches[0] };
86
48
  }
87
- return {
88
- resolveChatModel,
89
- validateChatModelId,
90
- DEFAULT_ROUTER_MODEL,
91
- DEFAULT_SANDBOX_OPENAI_MODEL,
92
- DEFAULT_ROUTER_BASE_URL
93
- };
49
+ return { succeeded: false, error: `Model is not available: ${cleaned}` };
94
50
  }
95
51
  function cleanModelId(value) {
96
52
  if (typeof value !== "string") return void 0;
@@ -110,14 +66,11 @@ function catalogIdsForModel(model) {
110
66
  }
111
67
  return [...ids];
112
68
  }
113
- function canonicalModelIdOrUndefined(model) {
114
- if (typeof model.id !== "string" || !model.id.trim()) return void 0;
115
- return canonicalModelId(model);
116
- }
117
69
  export {
118
70
  catalogIdsForModel,
119
71
  cleanModelId,
120
- createChatModelResolution,
121
- isWellFormedModelId
72
+ isWellFormedModelId,
73
+ resolveChatModel,
74
+ validateChatModelId
122
75
  };
123
76
  //# sourceMappingURL=index.js.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/model-resolution/index.ts"],"sourcesContent":["/**\n * Chat-time model resolution: a precedence resolver and a fail-closed catalog\n * validator that sit on top of a product's boot-time model config.\n *\n * `resolveChatModel` picks the model id for a chat turn by precedence:\n * request id > env MODEL_NAME > provider default > sandbox default.\n *\n * `validateChatModelId` is the fail-closed gate: it returns a typed outcome and\n * accepts an id only if it is in the constructed allowlist OR served by the live\n * router catalog (loaded through an injected boundary). A bare id with no\n * provider prefix resolves to its canonical id only when the suffix is unique\n * across the catalog, so an ambiguous suffix is rejected rather than silently\n * assigned a provider.\n *\n * The product injects one value — `modelDefaults` — and supplies the catalog\n * loader per call. `ModelInfo` is the router /v1/models wire shape and\n * `canonicalModelId` the bare->prefixed id helper, both defined locally so this\n * engine module carries no UI-package coupling.\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\n/** Which execution path the chat turn runs on. Product-supplied per turn. */\nexport type ChatBackend = 'router' | 'sandbox'\n\nexport type ChatModelSource =\n | 'request'\n | 'env:MODEL_NAME'\n | 'default'\n | 'sandbox-default'\n\nexport interface ResolvedChatModel {\n backend: ChatBackend\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\n/**\n * The single product-injected seam.\n *\n * - `routerModel` / `sandboxOpenaiModel`: the two `DEFAULT_*` ids used by the\n * precedence ladder and seeded into the allowlist.\n * - `routerBaseUrl`: catalog endpoint base; overridable per validate call.\n * - `extraAllowlist`: additional ids accepted without a catalog round-trip.\n */\nexport interface ChatModelDefaults {\n routerModel: string\n sandboxOpenaiModel: string\n routerBaseUrl?: string\n extraAllowlist?: string[]\n}\n\nexport interface ResolveChatModelOptions {\n requestedModel?: string\n backend: ChatBackend\n /** Env to read (defaults to process.env). Inject for non-node runtimes. */\n env?: Record<string, string | undefined>\n}\n\nexport interface ValidateChatModelIdOptions {\n routerBaseUrl?: string\n /** Catalog loader. No default body is baked in; the consumer supplies it. */\n loadModels: LoadModels\n}\n\nexport interface ChatModelResolution {\n resolveChatModel: (options: ResolveChatModelOptions) => ResolvedChatModel\n validateChatModelId: (\n modelId: unknown,\n options: ValidateChatModelIdOptions,\n ) => Promise<ChatModelValidationResult>\n DEFAULT_ROUTER_MODEL: string\n DEFAULT_SANDBOX_OPENAI_MODEL: string\n DEFAULT_ROUTER_BASE_URL?: string\n}\n\nexport function createChatModelResolution(defaults: ChatModelDefaults): ChatModelResolution {\n const DEFAULT_ROUTER_MODEL = defaults.routerModel\n const DEFAULT_SANDBOX_OPENAI_MODEL = defaults.sandboxOpenaiModel\n const DEFAULT_ROUTER_BASE_URL = defaults.routerBaseUrl\n\n const allowlist = new Set(\n [\n DEFAULT_ROUTER_MODEL,\n DEFAULT_SANDBOX_OPENAI_MODEL,\n ...(defaults.extraAllowlist ?? []),\n ].filter((model): model is string => typeof model === 'string' && model.length > 0),\n )\n\n function resolveChatModel({\n requestedModel,\n backend,\n env = process.env,\n }: ResolveChatModelOptions): ResolvedChatModel {\n const selectedModel = cleanModelId(requestedModel)\n if (selectedModel) return { backend, model: selectedModel, source: 'request' }\n\n if (backend === 'router') {\n const routerModel = cleanModelId(env.MODEL_NAME)\n return {\n backend,\n model: routerModel ?? DEFAULT_ROUTER_MODEL,\n source: routerModel ? 'env:MODEL_NAME' : 'default',\n }\n }\n\n const sandboxModel = cleanModelId(env.MODEL_NAME)\n if (sandboxModel) return { backend, model: sandboxModel, source: 'env:MODEL_NAME' }\n\n const modelProvider = env.MODEL_PROVIDER\n ?? (env.TANGLE_API_KEY ? 'openai-compat' : env.OPENAI_API_KEY ? 'openai' : undefined)\n if (modelProvider === 'openai' || modelProvider === 'openai-compat') {\n return { backend, model: DEFAULT_SANDBOX_OPENAI_MODEL, source: 'default' }\n }\n\n return { backend, source: 'sandbox-default' }\n }\n\n async function validateChatModelId(\n modelId: unknown,\n {\n routerBaseUrl = DEFAULT_ROUTER_BASE_URL,\n loadModels,\n }: ValidateChatModelIdOptions,\n ): Promise<ChatModelValidationResult> {\n const cleaned = cleanModelId(modelId)\n if (!cleaned) {\n return { succeeded: false, error: 'Model id must be a non-empty string.' }\n }\n if (!isWellFormedModelId(cleaned)) {\n return { succeeded: false, error: `Model id is malformed: ${cleaned}` }\n }\n if (allowlist.has(cleaned)) {\n return { succeeded: true, value: cleaned }\n }\n if (typeof routerBaseUrl !== 'string' || routerBaseUrl.length === 0) {\n return { succeeded: false, error: 'Router base URL is required to validate against the catalog.' }\n }\n\n let catalog: ModelInfo[]\n try {\n catalog = await loadModels(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 // Exact match against any id the catalog serves (canonical or bare).\n const ids = new Set(catalog.flatMap(catalogIdsForModel))\n if (ids.has(cleaned)) {\n return { succeeded: true, value: cleaned }\n }\n\n // A bare request id (no provider prefix) may name a model the catalog only\n // serves under a provider-prefixed id (e.g. request \"gpt-5\" -> catalog\n // \"openai/gpt-5\"). Resolve it to the canonical id the router serves, but only\n // when the bare suffix is unique across the catalog -- an ambiguous suffix\n // (e.g. \"openai/x\" vs \"vertex/x\") stays rejected so we never silently pick a\n // provider for the caller.\n if (!cleaned.includes('/')) {\n const canonicalBySuffix = new Map<string, string[]>()\n for (const model of catalog) {\n const canonical = canonicalModelIdOrUndefined(model)\n if (!canonical || !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 const only = matches && matches.length === 1 ? matches[0] : undefined\n if (only) {\n return { succeeded: true, value: only }\n }\n }\n\n return { succeeded: false, error: `Model is not available: ${cleaned}` }\n }\n\n return {\n resolveChatModel,\n validateChatModelId,\n DEFAULT_ROUTER_MODEL,\n DEFAULT_SANDBOX_OPENAI_MODEL,\n DEFAULT_ROUTER_BASE_URL,\n }\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\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\n // The bare suffix of a provider-prefixed id (e.g. \"openai/gpt-5\" -> \"gpt-5\")\n // is NOT added here: a bare request id resolves to its canonical id only\n // through the uniqueness-gated path in validateChatModelId, so an ambiguous\n // suffix never slips through as an exact match.\n return [...ids]\n}\n\n/** The canonical id for a catalog entry, or undefined when the entry has no id. */\nfunction canonicalModelIdOrUndefined(model: ModelInfo): string | undefined {\n if (typeof model.id !== 'string' || !model.id.trim()) return undefined\n return canonicalModelId(model)\n}\n"],"mappings":"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","names":[]}
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":"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","names":[]}
@@ -82,6 +82,10 @@ interface ProfileOverlay {
82
82
  * `prompt.systemPrompt` while keeping base `prompt.instructions`. When unset,
83
83
  * the base prompt passes through unchanged. */
84
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[];
85
89
  /** Profile `name` override. When unset, the base name is kept. */
86
90
  name?: string;
87
91
  }
@@ -29,16 +29,28 @@ function composeAgentProfile(base, channels = {}, overlay = {}) {
29
29
  const userFiles = channels.userSkills ? userSkillMounts(channels.userSkills) : [];
30
30
  const overlayFiles = channels.filesPredicate ? userFiles.filter(channels.filesPredicate) : userFiles;
31
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;
32
35
  const overlayProfile = {
33
36
  ...overlay.name ? { name: overlay.name } : {},
34
- ...overlay.systemPrompt ? { prompt: { systemPrompt: overlay.systemPrompt } } : {},
37
+ ...Object.keys(promptOverlay).length > 0 ? { prompt: promptOverlay } : {},
35
38
  ...overlay.mcp ? { mcp: overlay.mcp } : {},
36
39
  resources: { files }
37
40
  };
38
41
  const merged = mergeAgentProfiles(base, overlayProfile);
39
42
  if (!merged)
40
43
  throw new Error("composeAgentProfile: mergeAgentProfiles returned undefined for a defined base");
41
- return merged;
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;
42
54
  }
43
55
  function stripComments(raw) {
44
56
  return raw.replace(/<!--[\s\S]*?-->/g, "").trim();
@@ -1 +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 /** 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 overlayProfile: AgentProfile = {\n ...(overlay.name ? { name: overlay.name } : {}),\n ...(overlay.systemPrompt ? { prompt: { systemPrompt: overlay.systemPrompt } } : {}),\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 merged\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;AAoEjB,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,iBAA+B;AAAA,IACnC,GAAI,QAAQ,OAAO,EAAE,MAAM,QAAQ,KAAK,IAAI,CAAC;AAAA,IAC7C,GAAI,QAAQ,eAAe,EAAE,QAAQ,EAAE,cAAc,QAAQ,aAAa,EAAE,IAAI,CAAC;AAAA,IACjF,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;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":[]}
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"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tangle-network/agent-app",
3
- "version": "0.19.0",
3
+ "version": "0.20.0",
4
4
  "packageManager": "pnpm@10.33.4",
5
5
  "description": "Application-shell framework for Tangle agent products: a bounded tool loop, the structured agent→app tool side channel, integration-hub client, per-workspace billing, and crypto — composed over the Tangle agent substrate through typed seams.",
6
6
  "keywords": [