@tangle-network/agent-app 0.60.208 → 0.60.210
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/agent-profiles/d1.js +2 -2
- package/dist/agent-profiles/index.js +2 -2
- package/dist/assistant/index.js +1 -1
- package/dist/chat-routes/index.js +3 -3
- package/dist/{chunk-QDKY5TBG.js → chunk-6CKOEPXS.js} +6 -6
- package/dist/chunk-6CKOEPXS.js.map +1 -0
- package/dist/{chunk-IJAXOQJU.js → chunk-LJASCU5V.js} +7 -2
- package/dist/chunk-LJASCU5V.js.map +1 -0
- package/dist/{chunk-JDEGS53O.js → chunk-LRW36WCF.js} +25 -2
- package/dist/chunk-LRW36WCF.js.map +1 -0
- package/dist/{chunk-HCFVEJQL.js → chunk-T3IO7QIW.js} +2 -2
- package/dist/chunk-WHK5CFDB.js +28 -0
- package/dist/chunk-WHK5CFDB.js.map +1 -0
- package/dist/intakes/drizzle.js +114 -6
- package/dist/intakes/drizzle.js.map +1 -1
- package/dist/intakes/index.d.ts +2 -6
- package/dist/intakes/index.js +6 -8
- package/dist/intakes/index.js.map +1 -1
- package/dist/intakes/model.d.ts +1 -2
- package/dist/preset-cloudflare/index.js +3 -4
- package/dist/preset-cloudflare/index.js.map +1 -1
- package/dist/public-consultation/index.d.ts +2 -2
- package/dist/sandbox/index.js +3 -3
- package/dist/stories/panels/fixtures.d.ts +3 -29
- package/dist/tools/index.js +3 -3
- package/dist/tools/render-ui-schema.d.ts +2 -2
- package/dist/web-react/index.js +3 -3
- package/dist/work-product/index.js +1 -1
- package/package.json +1 -16
- package/dist/IntakeInterview-OKT6DCBN.js +0 -8
- package/dist/IntakeInterview-OKT6DCBN.js.map +0 -1
- package/dist/chunk-2RCPPOQ4.js +0 -17
- package/dist/chunk-2RCPPOQ4.js.map +0 -1
- package/dist/chunk-I2CA5N7V.js +0 -254
- package/dist/chunk-I2CA5N7V.js.map +0 -1
- package/dist/chunk-IJAXOQJU.js.map +0 -1
- package/dist/chunk-JDEGS53O.js.map +0 -1
- package/dist/chunk-QDKY5TBG.js.map +0 -1
- package/dist/chunk-VWAEWOWK.js +0 -124
- package/dist/chunk-VWAEWOWK.js.map +0 -1
- package/dist/chunk-YZ6KQULN.js +0 -30
- package/dist/chunk-YZ6KQULN.js.map +0 -1
- package/dist/intakes/api.d.ts +0 -54
- package/dist/intakes/api.js +0 -63
- package/dist/intakes/api.js.map +0 -1
- package/dist/intakes-react/components/BrandMark.d.ts +0 -10
- package/dist/intakes-react/components/IntakeInterview.d.ts +0 -15
- package/dist/intakes-react/components/index.d.ts +0 -1
- package/dist/intakes-react/contracts.d.ts +0 -42
- package/dist/intakes-react/index.d.ts +0 -14
- package/dist/intakes-react/index.js +0 -8
- package/dist/intakes-react/index.js.map +0 -1
- package/dist/intakes-react/lazy.d.ts +0 -10
- package/dist/intakes-react/lazy.js +0 -9
- package/dist/intakes-react/lazy.js.map +0 -1
- /package/dist/{chunk-HCFVEJQL.js.map → chunk-T3IO7QIW.js.map} +0 -0
|
@@ -2,9 +2,9 @@ import {
|
|
|
2
2
|
ProfileConflictError,
|
|
3
3
|
parseProfileRevisionDiff,
|
|
4
4
|
serializeProfileRevisionDiff
|
|
5
|
-
} from "../chunk-
|
|
5
|
+
} from "../chunk-T3IO7QIW.js";
|
|
6
6
|
import "../chunk-I6BZVIRH.js";
|
|
7
|
-
import "../chunk-
|
|
7
|
+
import "../chunk-LJASCU5V.js";
|
|
8
8
|
import "../chunk-UGWQLQDS.js";
|
|
9
9
|
import "../chunk-E65YV7VV.js";
|
|
10
10
|
|
|
@@ -31,12 +31,12 @@ import {
|
|
|
31
31
|
selfMountTable,
|
|
32
32
|
serializeProfileRevisionDiff,
|
|
33
33
|
switchProfile
|
|
34
|
-
} from "../chunk-
|
|
34
|
+
} from "../chunk-T3IO7QIW.js";
|
|
35
35
|
import {
|
|
36
36
|
normalizeProfileName,
|
|
37
37
|
parseProfileSwitch
|
|
38
38
|
} from "../chunk-I6BZVIRH.js";
|
|
39
|
-
import "../chunk-
|
|
39
|
+
import "../chunk-LJASCU5V.js";
|
|
40
40
|
import "../chunk-UGWQLQDS.js";
|
|
41
41
|
import "../chunk-E65YV7VV.js";
|
|
42
42
|
export {
|
package/dist/assistant/index.js
CHANGED
|
@@ -2,8 +2,8 @@ import {
|
|
|
2
2
|
ReasoningRow,
|
|
3
3
|
chatToolCallPart
|
|
4
4
|
} from "../chunk-KNTRS7OZ.js";
|
|
5
|
-
import "../chunk-KMIDRUUH.js";
|
|
6
5
|
import "../chunk-3HUFJ5LO.js";
|
|
6
|
+
import "../chunk-KMIDRUUH.js";
|
|
7
7
|
import "../chunk-FBVLEGEG.js";
|
|
8
8
|
import "../chunk-Y6PRDUFJ.js";
|
|
9
9
|
import "../chunk-GEYACSFW.js";
|
|
@@ -52,12 +52,12 @@ import {
|
|
|
52
52
|
} from "../chunk-ZLX4BA27.js";
|
|
53
53
|
import "../chunk-VBRUG4N5.js";
|
|
54
54
|
import "../chunk-LWSJK546.js";
|
|
55
|
-
import "../chunk-
|
|
55
|
+
import "../chunk-6CKOEPXS.js";
|
|
56
56
|
import "../chunk-6A7MYOUI.js";
|
|
57
57
|
import "../chunk-O7UX55EU.js";
|
|
58
|
-
import "../chunk-
|
|
58
|
+
import "../chunk-WHK5CFDB.js";
|
|
59
59
|
import "../chunk-S5SRJJQG.js";
|
|
60
|
-
import "../chunk-
|
|
60
|
+
import "../chunk-LJASCU5V.js";
|
|
61
61
|
import "../chunk-UGWQLQDS.js";
|
|
62
62
|
import "../chunk-E65YV7VV.js";
|
|
63
63
|
import {
|
|
@@ -3,13 +3,13 @@ import {
|
|
|
3
3
|
createMcpToolHandler
|
|
4
4
|
} from "./chunk-6A7MYOUI.js";
|
|
5
5
|
import {
|
|
6
|
-
|
|
7
|
-
} from "./chunk-
|
|
6
|
+
parseRenderUiSchema
|
|
7
|
+
} from "./chunk-WHK5CFDB.js";
|
|
8
8
|
import {
|
|
9
9
|
buildAppToolOpenAITools,
|
|
10
10
|
findCustomTool,
|
|
11
11
|
isAppToolName
|
|
12
|
-
} from "./chunk-
|
|
12
|
+
} from "./chunk-LJASCU5V.js";
|
|
13
13
|
import {
|
|
14
14
|
ToolInputError
|
|
15
15
|
} from "./chunk-E65YV7VV.js";
|
|
@@ -94,8 +94,8 @@ async function dispatchAppTool(toolName2, rawArgs, ctx, opts) {
|
|
|
94
94
|
return { ok: true, result: { followupId: r2.id, dueDate: r2.dueDate, deduped: r2.deduped } };
|
|
95
95
|
}
|
|
96
96
|
if (toolName2 === "render_ui") {
|
|
97
|
-
|
|
98
|
-
const r2 = await opts.handlers.renderUi({ title: String(rawArgs.title ?? ""), schema
|
|
97
|
+
const schema = parseRenderUiSchema(rawArgs.schema);
|
|
98
|
+
const r2 = await opts.handlers.renderUi({ title: String(rawArgs.title ?? ""), schema }, ctx);
|
|
99
99
|
opts.onProduced?.({ type: "artifact", path: r2.path, content: r2.content });
|
|
100
100
|
return { ok: true, result: { path: r2.path } };
|
|
101
101
|
}
|
|
@@ -212,4 +212,4 @@ export {
|
|
|
212
212
|
createAppToolRuntimeExecutor,
|
|
213
213
|
handleAppToolRequest
|
|
214
214
|
};
|
|
215
|
-
//# sourceMappingURL=chunk-
|
|
215
|
+
//# sourceMappingURL=chunk-6CKOEPXS.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/tools/gating.ts","../src/tools/dispatch.ts","../src/tools/runtime.ts","../src/tools/http.ts"],"sourcesContent":["/**\n * Capability gating — compose an agent session's tool surface from a\n * product-defined capability registry.\n *\n * Products that let users pick what an agent can do (a \"Studio\" agent with the\n * full build toolset vs a clean \"Assistant\" with none) all need the same\n * mechanics: a registry of named capabilities, each unlocking proposal types\n * and/or named tool groups, resolved against the product's\n * {@link AppToolTaxonomy} into the concrete tools to expose. The mechanics are\n * generic and live here; the capability VOCABULARY (ids, labels, which\n * proposal types, which tool groups) is the product's.\n */\n\nimport type { AppToolTaxonomy } from './types'\n\n/** One toggleable tool group in a product's capability registry. */\nexport interface ToolCapability {\n id: string\n label: string\n description: string\n /** Proposal types this capability unlocks (intersected with the taxonomy,\n * so a capability can never widen the product's proposal surface). */\n proposalTypes?: readonly string[]\n /** Unlocks every taxonomy proposal type beyond the base set — the domain's\n * specialized long tail (risk_assessment, vuln_report, …). */\n domainActions?: boolean\n /** Named product tool groups (e.g. 'sandbox', 'integrations') this\n * capability unlocks. The vocabulary is the product's; the resolver only\n * unions them. */\n toolGroups?: readonly string[]\n}\n\n/** Resolve options for determining tool capabilities based on taxonomy, capabilities, and enabled IDs */\nexport interface ResolveToolCapabilitiesOptions {\n taxonomy: AppToolTaxonomy\n /** The product's full capability registry. */\n capabilities: readonly ToolCapability[]\n /** Enabled capability ids. `undefined` means full access (legacy callers\n * that don't send a capability set); an explicit `[]` means a pure chat\n * agent with no tools. Unknown ids are ignored. */\n enabled: readonly string[] | undefined\n /** The shared base proposal types `domainActions` excludes. Defaults to\n * every type some capability names explicitly via `proposalTypes` — i.e.\n * \"domain actions\" are the taxonomy types no capability claims. */\n baseProposalTypes?: readonly string[]\n}\n\n/** Describe resolved capabilities including proposal types and product tool groups to expose */\nexport interface ResolvedToolCapabilities {\n /** Proposal types to keep — feed to {@link restrictTaxonomy}. */\n proposalTypes: string[]\n /** Product tool groups to expose (deduped union across enabled caps). */\n toolGroups: string[]\n}\n\n/**\n * Resolve an enabled capability-id set against a taxonomy into the concrete\n * tool surface. Fail-closed: only types present in the taxonomy survive, and\n * an empty `enabled` set yields no tools at all.\n */\nexport function resolveToolCapabilities(\n opts: ResolveToolCapabilitiesOptions,\n): ResolvedToolCapabilities {\n const { taxonomy, capabilities, enabled } = opts\n if (enabled === undefined) {\n return {\n proposalTypes: [...taxonomy.proposalTypes],\n toolGroups: [...new Set(capabilities.flatMap((c) => c.toolGroups ?? []))],\n }\n }\n const base = new Set(\n opts.baseProposalTypes ?? capabilities.flatMap((c) => c.proposalTypes ?? []),\n )\n const domainTypes = taxonomy.proposalTypes.filter((t) => !base.has(t))\n const byId = new Map(capabilities.map((c) => [c.id, c]))\n\n const proposalTypes = new Set<string>()\n const toolGroups = new Set<string>()\n for (const id of enabled) {\n const cap = byId.get(id)\n if (!cap) continue\n for (const t of cap.proposalTypes ?? []) {\n if (taxonomy.proposalTypes.includes(t)) proposalTypes.add(t)\n }\n if (cap.domainActions) for (const t of domainTypes) proposalTypes.add(t)\n for (const g of cap.toolGroups ?? []) toolGroups.add(g)\n }\n return { proposalTypes: [...proposalTypes], toolGroups: [...toolGroups] }\n}\n\n/**\n * Restrict a taxonomy to a subset of proposal types, intersecting the\n * regulated subset too — the regulated label survives restriction, so a\n * narrowed agent can never launder a regulated type into an unregulated one.\n */\nexport function restrictTaxonomy(\n taxonomy: AppToolTaxonomy,\n allowed: readonly string[],\n): AppToolTaxonomy {\n const allow = new Set(allowed)\n return {\n proposalTypes: taxonomy.proposalTypes.filter((t) => allow.has(t)),\n regulatedTypes: taxonomy.regulatedTypes.filter((t) => allow.has(t)),\n }\n}\n","import { ToolInputError } from './errors'\nimport { isAppToolName } from './openai'\nimport { findCustomTool, type AppToolDefinition } from './registry'\nimport { parseRenderUiSchema } from './render-ui-schema'\nimport type {\n AppToolContext,\n AppToolHandlers,\n AppToolOutcome,\n AppToolProducedEvent,\n AppToolTaxonomy,\n} from './types'\n\n/** Define options for dispatching tools including handlers, taxonomy, custom tools, and approval policies */\nexport interface DispatchOptions {\n handlers: AppToolHandlers\n taxonomy: AppToolTaxonomy\n /** Product-registered tools beyond the four built-ins. A called name that is\n * not a built-in is dispatched to the matching {@link AppToolDefinition.execute}\n * through this same validation/outcome path. */\n customTools?: readonly AppToolDefinition[]\n /** Per-call approval policy. When provided it OVERRIDES the static\n * `taxonomy.regulatedTypes` membership check, so products can gate by\n * cost threshold, environment, or first-use instead of always/never.\n * Fail-closed: a predicate that throws counts as \"approval required\". */\n needsApproval?: (type: string, args: { title: string; description: string | null }, ctx: AppToolContext) => boolean | Promise<boolean>\n /** Called at the real side-effect site for proposals (proposal_created) and\n * generated views (artifact) so a consumer's completion oracle credits\n * persisted state. Omit when produced state isn't tracked. */\n onProduced?: (event: AppToolProducedEvent) => void\n}\n\n/**\n * The ONE place an app-tool call is validated, dispatched to the product's\n * handler, and turned into an {@link AppToolOutcome} + produced events. Shared\n * by the HTTP route layer and the agent-runtime executor so both paths apply\n * identical validation and identical side effects. A {@link ToolInputError}\n * (bad input the agent can correct) and any other throw both become\n * `{ ok: false }` — a tool call never silently \"succeeds\" without its effect.\n */\nexport async function dispatchAppTool(\n toolName: string,\n rawArgs: Record<string, unknown>,\n ctx: AppToolContext,\n opts: DispatchOptions,\n): Promise<AppToolOutcome> {\n try {\n if (!isAppToolName(toolName)) {\n const custom = findCustomTool(toolName, opts.customTools)\n if (!custom) return { ok: false, code: 'unknown_tool', message: `${toolName} is not an app tool.` }\n // Custom tools own their own arg validation (their execute throws\n // ToolInputError for correctable input); the outer try/catch maps it.\n const result = await custom.execute(rawArgs, ctx)\n return { ok: true, result }\n }\n\n if (toolName === 'submit_proposal') {\n const type = String(rawArgs.type ?? '').trim()\n const title = String(rawArgs.title ?? '').trim()\n if (!type || !opts.taxonomy.proposalTypes.includes(type)) {\n return { ok: false, code: 'invalid_type', message: `type must be one of: ${opts.taxonomy.proposalTypes.join(', ')}.` }\n }\n if (!title) return { ok: false, code: 'missing_title', message: 'title is required.' }\n const description = rawArgs.description == null ? null : String(rawArgs.description)\n // Approval policy runs BEFORE the handler so the decision can gate the\n // side effect itself, not merely re-label it afterwards.\n let regulated = opts.taxonomy.regulatedTypes.includes(type)\n if (opts.needsApproval) {\n try {\n regulated = await opts.needsApproval(type, { title, description }, ctx)\n } catch {\n regulated = true // fail-closed: a broken policy means approval required\n }\n }\n const r = await opts.handlers.submitProposal({ type, title, description, regulated }, ctx)\n // Pass the handler's result through: products with immediate-execute\n // proposal types return status 'executed' plus their own fields\n // (e.g. datasetId) — the model must see what actually happened, not a\n // hard-coded \"queued for approval\".\n const { proposalId, deduped, status, ...extra } = r\n const effectiveStatus = status ?? 'queued_for_approval'\n opts.onProduced?.({\n type: 'proposal_created',\n proposalId,\n title,\n status: effectiveStatus === 'executed' ? 'executed' : 'pending',\n content: description ?? undefined,\n })\n return { ok: true, result: { ...extra, status: effectiveStatus, proposalId, deduped, regulated } }\n }\n\n if (toolName === 'schedule_followup') {\n const r = await opts.handlers.scheduleFollowup(\n { title: String(rawArgs.title ?? ''), dueDate: String(rawArgs.dueDate ?? ''), priority: rawArgs.priority as string | undefined },\n ctx,\n )\n return { ok: true, result: { followupId: r.id, dueDate: r.dueDate, deduped: r.deduped } }\n }\n\n if (toolName === 'render_ui') {\n const schema = parseRenderUiSchema(rawArgs.schema)\n const r = await opts.handlers.renderUi({ title: String(rawArgs.title ?? ''), schema }, ctx)\n opts.onProduced?.({ type: 'artifact', path: r.path, content: r.content })\n return { ok: true, result: { path: r.path } }\n }\n\n // add_citation\n const r = await opts.handlers.addCitation(\n { path: String(rawArgs.path ?? ''), quote: String(rawArgs.quote ?? ''), label: rawArgs.label as string | undefined },\n ctx,\n )\n return { ok: true, result: { citationId: r.citationId, path: r.path } }\n } catch (err) {\n if (err instanceof ToolInputError) return { ok: false, code: err.code, message: err.message, status: err.status }\n return { ok: false, code: 'app_tool_error', message: err instanceof Error ? err.message : String(err), status: 500 }\n }\n}\n\n/** HTTP status for a failed outcome — the handler's `ToolInputError.status`\n * when present, else 400 for a validation reject. */\nexport function outcomeStatus(outcome: Extract<AppToolOutcome, { ok: false }>): number {\n return outcome.status ?? 400\n}\n","import { dispatchAppTool, type DispatchOptions } from './dispatch'\nimport type { AppToolContext, AppToolOutcome } from './types'\n\n/** Executes an app-tool call the model emits on the agent-runtime chat path.\n * Plug into `runChatThroughRuntime({ appToolExecutor })` (or any loop that\n * dispatches function tool_calls). */\nexport type AppToolRuntimeExecutor = (call: {\n toolName: string\n args: Record<string, unknown>\n}) => Promise<AppToolOutcome>\n\n/** Define options for executing runtime tasks with a trusted per-turn context */\nexport interface RuntimeExecutorOptions extends DispatchOptions {\n /** The trusted per-turn context — supplied directly (not from headers), since\n * the runtime path has no HTTP request. */\n ctx: AppToolContext\n}\n\n/**\n * Build the runtime executor for one turn. The agent-runtime backend must also\n * advertise the tools (`buildAppToolOpenAITools(taxonomy)` on the backend's\n * `tools`) for the model to call them; this executor fulfils each call against\n * the product's handlers and emits produced events via `opts.onProduced`.\n */\nexport function createAppToolRuntimeExecutor(opts: RuntimeExecutorOptions): AppToolRuntimeExecutor {\n return ({ toolName, args }) => dispatchAppTool(toolName, args, opts.ctx, opts)\n}\n","import { authenticateToolRequest, type ToolHeaderNames } from './auth'\nimport { dispatchAppTool, outcomeStatus, type DispatchOptions } from './dispatch'\nimport { buildAppToolOpenAITools, type AppToolName } from './openai'\nimport type { AppToolDefinition } from './registry'\nimport { createMcpToolHandler, type McpToolCallContent } from './mcp-rpc'\nimport type { AppToolOutcome } from './types'\n\n/** Define options for handling tool requests including tool identification and token verification */\nexport interface HandleToolRequestOptions extends DispatchOptions {\n /** Which app tool this route serves — a built-in name or a product-registered\n * {@link AppToolDefinition} (auto-added to `customTools` for dispatch). */\n tool: AppToolName | AppToolDefinition\n /** Verify the bearer capability token belongs to the header user. */\n verifyToken: (userId: string, bearer: string) => Promise<boolean>\n headerNames?: ToolHeaderNames\n /** Optional model-facing description used by the MCP tools/list response. */\n description?: string\n /** Product-specific values for the schedule_followup.priority schema. */\n priorityValues?: readonly string[]\n /** Optional success-message builder for a friendlier tool result. */\n message?: (result: unknown) => string\n}\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return typeof value === 'object' && value !== null && !Array.isArray(value)\n}\n\nfunction isMcpRequest(request: Request, value: unknown): boolean {\n // A valid request declares jsonrpc. An incomplete envelope has no reliable\n // body marker, so use the Streamable HTTP Accept contract as the explicit\n // transport discriminator before falling back to direct tool arguments.\n const acceptsMcp = request.headers\n .get('accept')\n ?.split(',')\n .some((part) => part.trim().toLowerCase() === 'text/event-stream')\n return (\n isRecord(value)\n && (\n 'jsonrpc' in value\n || acceptsMcp === true\n || request.headers.has('mcp-session-id')\n || request.headers.has('mcp-protocol-version')\n )\n )\n}\n\nfunction toolName(tool: AppToolName | AppToolDefinition): string {\n return typeof tool === 'string' ? tool : tool.name\n}\n\nfunction dispatchOptions(opts: HandleToolRequestOptions): DispatchOptions {\n return {\n ...opts,\n customTools:\n typeof opts.tool === 'string' ? opts.customTools : [...(opts.customTools ?? []), opts.tool],\n }\n}\n\nfunction mcpToolManifest(opts: HandleToolRequestOptions): {\n name: string\n description: string\n inputSchema: Record<string, unknown>\n} {\n const name = toolName(opts.tool)\n const tools = buildAppToolOpenAITools(opts.taxonomy, {\n ...(opts.description && typeof opts.tool === 'string'\n ? { descriptions: { [opts.tool]: opts.description } }\n : {}),\n ...(opts.priorityValues ? { priorityValues: opts.priorityValues } : {}),\n ...(typeof opts.tool === 'string' ? {} : { customTools: [opts.tool] }),\n })\n const definition = tools.find((entry) => entry.function.name === name)\n if (!definition) {\n throw new Error(`No MCP manifest exists for app tool ${name}`)\n }\n return {\n name,\n description: definition.function.description,\n inputSchema: definition.function.parameters,\n }\n}\n\nfunction mcpToolResult(outcome: AppToolOutcome, opts: HandleToolRequestOptions): McpToolCallContent {\n if (!outcome.ok) {\n return {\n content: [{ type: 'text', text: `${outcome.code}: ${outcome.message}` }],\n isError: true,\n }\n }\n\n const value = outcome.result\n const payload = value !== null && typeof value === 'object' && !Array.isArray(value)\n ? { ok: true, ...value }\n : { ok: true, value }\n return {\n content: [{ type: 'text', text: JSON.stringify({\n ...payload,\n ...(opts.message ? { message: opts.message(value) } : {}),\n }) }],\n }\n}\n\nfunction createAppToolMcpHandler(\n authContext: Parameters<DispatchOptions['handlers']['submitProposal']>[1],\n opts: HandleToolRequestOptions,\n): (request: Request) => Promise<Response> {\n const manifest = mcpToolManifest(opts)\n return createMcpToolHandler({\n serverInfo: { name: 'agent-app-tool', version: '1' },\n tools: [{\n ...manifest,\n run: (args) => dispatchAppTool(manifest.name, args, authContext, dispatchOptions(opts)),\n }],\n buildEnv: () => ({}),\n formatResult: (result) => mcpToolResult(result as AppToolOutcome, opts),\n })\n}\n\n/**\n * Handle one app-tool HTTP request end to end — the sandbox MCP path. The\n * agent's per-turn HTTP MCP server POSTs here; this authenticates (header user\n * + capability token), reads the args (MCP-alias tolerant), dispatches to the\n * product handler, and returns a JSON Response. A product's route file becomes\n * a one-liner: `export const action = ({ request }) => handleAppToolRequest(request, cfg)`.\n */\nexport async function handleAppToolRequest(request: Request, opts: HandleToolRequestOptions): Promise<Response> {\n if (request.method !== 'POST') return Response.json({ error: 'Method not allowed' }, { status: 405 })\n\n const auth = await authenticateToolRequest(request, { verifyToken: opts.verifyToken, headerNames: opts.headerNames })\n if (!auth.ok) return auth.response\n\n let body: unknown\n try {\n body = await request.clone().json()\n } catch {\n return Response.json({ error: 'Invalid JSON' }, { status: 400 })\n }\n\n // The same route supports the direct app-tool HTTP shape used by the\n // OpenAI/runtime surfaces and stateless Streamable HTTP MCP JSON-RPC used by\n // OpenCode. Keeping both at this boundary prevents every consumer from\n // writing a second route just to translate protocol envelopes.\n if (isMcpRequest(request, body)) {\n return createAppToolMcpHandler(auth.ctx, opts)(request)\n }\n\n if (!isRecord(body)) return Response.json({ error: 'Invalid JSON' }, { status: 400 })\n const args = (body.args ?? body.arguments ?? body) as Record<string, unknown>\n\n // A custom tool passed as `tool` is registered for this dispatch so the route\n // file stays a one-liner (no separate customTools wiring needed).\n const toolName = typeof opts.tool === 'string' ? opts.tool : opts.tool.name\n const outcome = await dispatchAppTool(toolName, args, auth.ctx, dispatchOptions(opts))\n if (!outcome.ok) {\n return Response.json({ error: outcome.code, message: outcome.message }, { status: outcomeStatus(outcome) })\n }\n const payload = outcome.result as Record<string, unknown>\n return Response.json({ ok: true, ...payload, ...(opts.message ? { message: opts.message(outcome.result) } : {}) })\n}\n"],"mappings":"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","names":["toolName","r","toolName","toolName"]}
|
|
@@ -64,7 +64,12 @@ ${OPENUI_INTERACTIVE_AUTHORING_GUIDE}` : ""),
|
|
|
64
64
|
$defs: OPENUI_NODE_JSON_SCHEMA.$defs,
|
|
65
65
|
properties: {
|
|
66
66
|
title: { type: "string" },
|
|
67
|
-
schema: {
|
|
67
|
+
schema: {
|
|
68
|
+
anyOf: [
|
|
69
|
+
{ $ref: "#/$defs/node" },
|
|
70
|
+
{ type: "string", description: "JSON text encoding exactly one OpenUI node object. Encode once; do not double-encode." }
|
|
71
|
+
]
|
|
72
|
+
}
|
|
68
73
|
},
|
|
69
74
|
required: ["title", "schema"]
|
|
70
75
|
}
|
|
@@ -112,4 +117,4 @@ export {
|
|
|
112
117
|
customToolToOpenAI,
|
|
113
118
|
findCustomTool
|
|
114
119
|
};
|
|
115
|
-
//# sourceMappingURL=chunk-
|
|
120
|
+
//# sourceMappingURL=chunk-LJASCU5V.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/tools/openai.ts","../src/tools/registry.ts"],"sourcesContent":["import { OPENUI_INTERACTIVE_AUTHORING_GUIDE } from '../openui/authoring'\nimport { OPENUI_NODE_JSON_SCHEMA, OPENUI_NODE_TYPES } from '@tangle-network/ui/openui-schema'\nimport type { AppToolTaxonomy, BuildAppToolsOptions } from './types'\n\n/** The four canonical app-tool names. Stable identifiers the model calls in\n * both the sandbox (MCP server name) and runtime (function-tool name) paths. */\nexport const APP_TOOL_NAMES = ['submit_proposal', 'schedule_followup', 'render_ui', 'add_citation'] as const\n/** Resolve a valid application tool name from the predefined list of tool names */\nexport type AppToolName = (typeof APP_TOOL_NAMES)[number]\n\nconst NAME_SET = new Set<string>(APP_TOOL_NAMES)\n/** Determine if a string matches a valid application tool name */\nexport function isAppToolName(name: string): name is AppToolName {\n return NAME_SET.has(name)\n}\n\n/** A minimal OpenAI Chat Completions function-tool shape — structurally\n * compatible with `@tangle-network/agent-runtime`'s `OpenAIChatTool` without\n * importing it (keeps this package runtime-free). */\nexport interface OpenAIFunctionTool {\n type: 'function'\n function: {\n name: string\n description: string\n parameters: Record<string, unknown>\n }\n}\n\n/**\n * Build the four app tools in OpenAI function-tool shape. `submit_proposal`'s\n * `type` enum is the product's {@link AppToolTaxonomy.proposalTypes}; the\n * model-facing descriptions and the follow-up priority enum default to the\n * Tangle reference vocabulary and can be retuned via {@link BuildAppToolsOptions}\n * (the tool names + JSON-Schema shapes stay fixed — they are mechanism). Pass\n * the result to the agent-runtime backend's `tools`.\n */\nexport function buildAppToolOpenAITools(\n taxonomy: AppToolTaxonomy,\n opts?: BuildAppToolsOptions,\n): OpenAIFunctionTool[] {\n const d = opts?.descriptions\n const priorityValues = opts?.priorityValues ?? ['low', 'medium', 'high']\n const custom: OpenAIFunctionTool[] = (opts?.customTools ?? []).map((t) => ({\n type: 'function',\n function: { name: t.name, description: t.description, parameters: t.parameters },\n }))\n return [\n {\n type: 'function',\n function: {\n name: 'submit_proposal',\n description:\n d?.submit_proposal ??\n 'Route a regulated or state-changing action to a human for approval (a recommendation, contacting/soliciting a contact, outreach, a record/account change, scheduling). Queues it for a named certified human to approve before it executes.',\n parameters: {\n type: 'object',\n properties: {\n type: { type: 'string', enum: [...taxonomy.proposalTypes] },\n title: { type: 'string', description: 'Short label for the approval queue.' },\n description: { type: 'string', description: 'The full drafted message/recommendation, with sources.' },\n },\n required: ['type', 'title'],\n },\n },\n },\n {\n type: 'function',\n function: {\n name: 'schedule_followup',\n description:\n d?.schedule_followup ??\n 'Register a dated cadence step (a reminder, chase, or check-in) on the follow-up calendar. Executes immediately.',\n parameters: {\n type: 'object',\n properties: {\n title: { type: 'string' },\n dueDate: { type: 'string', description: 'ISO date YYYY-MM-DD.' },\n priority: { type: 'string', enum: [...priorityValues] },\n },\n required: ['title', 'dueDate'],\n },\n },\n },\n {\n type: 'function',\n function: {\n name: 'render_ui',\n description:\n (d?.render_ui ??\n `Show a generated view live in the workspace. Validates the OpenUI JSON and persists the artifact. Use only these nodes: ${OPENUI_NODE_TYPES.join(', ')}. Stack, grid, and card may contain nested children. Executes immediately.`) +\n (opts?.interactiveUi ? `\\n\\n${OPENUI_INTERACTIVE_AUTHORING_GUIDE}` : ''),\n parameters: {\n type: 'object',\n $defs: OPENUI_NODE_JSON_SCHEMA.$defs,\n properties: {\n title: { type: 'string' },\n schema: {\n anyOf: [\n { $ref: '#/$defs/node' },\n { type: 'string', description: 'JSON text encoding exactly one OpenUI node object. Encode once; do not double-encode.' },\n ],\n },\n },\n required: ['title', 'schema'],\n },\n },\n },\n {\n type: 'function',\n function: {\n name: 'add_citation',\n description:\n d?.add_citation ??\n 'Anchor a grounding reference: the exact quote from a file backing a figure or claim. Verifies the quote appears in the file. Executes immediately.',\n parameters: {\n type: 'object',\n properties: {\n path: { type: 'string', description: 'The vault file path.' },\n quote: { type: 'string', description: 'The exact text from it.' },\n },\n required: ['path', 'quote'],\n },\n },\n },\n ...custom,\n ]\n}\n","import { isAppToolName } from './openai'\nimport type { AppToolContext } from './types'\nimport type { OpenAIFunctionTool } from './openai'\n\n/**\n * A product-defined app tool — the open registration seam.\n *\n * The four built-ins (`submit_proposal`/`schedule_followup`/`render_ui`/\n * `add_citation`) are mechanism and stay hard-typed. This is how a product adds\n * a fifth+ tool (e.g. gtm-agent's `set_config`) WITHOUT forking the shell: the\n * `name` + JSON-Schema `parameters` are what the model sees, and `execute` is\n * dispatched through the SAME validation/outcome path as the built-ins — a\n * thrown {@link ToolInputError} becomes a correctable 4xx, any other throw an\n * internal error, and a tool call never silently \"succeeds\" without its effect.\n */\nexport interface AppToolDefinition<Args = Record<string, unknown>> {\n /** Stable identifier the model calls (and the MCP server name). Must not\n * collide with a built-in app tool. */\n name: string\n /** Model-facing description. */\n description: string\n /** JSON-Schema for the parameters (the OpenAI `function.parameters` object). */\n parameters: Record<string, unknown>\n /** Default route path the per-turn MCP server / HTTP handler is mounted at.\n * Overridable per call via `paths`/`buildHttpMcpServer`. */\n path?: string\n /** Fulfil the call; the return value is the tool result the model sees.\n * `ctx` is the trusted per-turn identity (never from tool args). */\n execute: (args: Args, ctx: AppToolContext) => Promise<unknown> | unknown\n}\n\n/**\n * Validate + brand a product tool definition. Throws when the name is empty or\n * collides with a built-in (those are reserved mechanism). Identity otherwise —\n * call it at module scope so a bad definition fails at boot, not first use.\n */\nexport function defineAppTool<Args = Record<string, unknown>>(def: AppToolDefinition<Args>): AppToolDefinition<Args> {\n const name = def.name?.trim()\n if (!name) throw new Error('defineAppTool: name is required')\n if (isAppToolName(name)) throw new Error(`defineAppTool: \"${name}\" is a built-in app tool — choose a different name`)\n if (typeof def.execute !== 'function') throw new Error(`defineAppTool: \"${name}\" needs an execute() handler`)\n return def\n}\n\n/** The OpenAI function-tool def for a custom tool — appended to the built-ins by\n * `buildAppToolOpenAITools`. */\nexport function customToolToOpenAI(def: AppToolDefinition): OpenAIFunctionTool {\n return { type: 'function', function: { name: def.name, description: def.description, parameters: def.parameters } }\n}\n\n/** Find a registered custom tool by the name the model called. */\nexport function findCustomTool(\n name: string,\n tools: readonly AppToolDefinition[] | undefined,\n): AppToolDefinition | undefined {\n return tools?.find((t) => t.name === name)\n}\n"],"mappings":"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","names":[]}
|
|
@@ -107,6 +107,24 @@ function optionExists(question, value) {
|
|
|
107
107
|
return (question.options ?? []).some((option) => option.value === value);
|
|
108
108
|
}
|
|
109
109
|
|
|
110
|
+
// src/intakes/completion.ts
|
|
111
|
+
function emptyPayload(graph) {
|
|
112
|
+
return { graphId: graph.id, answers: {} };
|
|
113
|
+
}
|
|
114
|
+
function withAnswer(payload, questionId, value) {
|
|
115
|
+
return { ...payload, answers: { ...payload.answers, [questionId]: value } };
|
|
116
|
+
}
|
|
117
|
+
function payloadComplete(graph, payload) {
|
|
118
|
+
if (payload.graphId !== graph.id) return false;
|
|
119
|
+
return isComplete(graph, payload.answers);
|
|
120
|
+
}
|
|
121
|
+
function payloadIsStale(graph, payload) {
|
|
122
|
+
return payload.graphId !== graph.id;
|
|
123
|
+
}
|
|
124
|
+
function markComplete(payload, at = /* @__PURE__ */ new Date()) {
|
|
125
|
+
return { ...payload, completedAt: at.toISOString() };
|
|
126
|
+
}
|
|
127
|
+
|
|
110
128
|
export {
|
|
111
129
|
getQuestion,
|
|
112
130
|
hasAnswer,
|
|
@@ -114,6 +132,11 @@ export {
|
|
|
114
132
|
nextQuestion,
|
|
115
133
|
isComplete,
|
|
116
134
|
intakeProgress,
|
|
117
|
-
reachableQuestions
|
|
135
|
+
reachableQuestions,
|
|
136
|
+
emptyPayload,
|
|
137
|
+
withAnswer,
|
|
138
|
+
payloadComplete,
|
|
139
|
+
payloadIsStale,
|
|
140
|
+
markComplete
|
|
118
141
|
};
|
|
119
|
-
//# sourceMappingURL=chunk-
|
|
142
|
+
//# sourceMappingURL=chunk-LRW36WCF.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/intakes/model.ts","../src/intakes/completion.ts"],"sourcesContent":["/**\n * Pure intake MODEL — the question-graph and the answer/completion algebra a\n * one-question-at-a-time interview runs on. Zero dependencies: no drizzle, no\n * env, no react, no I/O. The DB layer (`./intakes/drizzle`) builds on\n * these functions; this leaf imports nothing back, so a consumer can pull just\n * the intake math.\n *\n * An intake is an ordered list of questions, optionally branching: a question\n * can declare `next` as a function of the answers so far, so the next prompt\n * depends on what was said (e.g. \"do you have a website?\" → no → skip the URL\n * question). Traversal is pure and deterministic — `nextQuestion(graph, answers)`\n * folds the answers into the single question to ask next (or null = done), and\n * `isComplete` is true exactly when every REQUIRED reachable question has a\n * valid answer.\n *\n * Answers are a flat `Record<questionId, value>`. Each question declares an\n * answer `type` that `validateAnswer` checks against — the same validation the\n * UI runs before advancing and the store runs before persisting, so an invalid\n * answer can never enter the payload.\n */\n\n/** The kinds of answer a question accepts. */\nexport type IntakeAnswerType =\n | 'text'\n | 'long-text'\n | 'single-select'\n | 'multi-select'\n | 'boolean'\n | 'number'\n | 'url'\n | 'email'\n\n/** A selectable option for single/multi-select questions. */\nexport interface IntakeOption {\n value: string\n label: string\n}\n\n/** A flat map of answers keyed by question id. */\nexport type IntakeAnswers = Record<string, IntakeAnswerValue>\n\n/** Any value an answer can hold; the type is validated per-question. */\nexport type IntakeAnswerValue = string | string[] | number | boolean | null\n\n/**\n * One question in the graph. `next` (optional) makes the graph branch: given\n * the answers so far it returns the id of the question to ask next, or null to\n * end the interview early. With no `next`, traversal falls through to the next\n * question in declaration order.\n */\nexport interface IntakeQuestion {\n id: string\n /** The prompt the interviewer asks. */\n prompt: string\n type: IntakeAnswerType\n /** Required questions gate completion; optional ones may be skipped. */\n required?: boolean\n /** Help text shown under the prompt. */\n help?: string\n /** Options for single/multi-select questions. */\n options?: IntakeOption[]\n /** Min length (text) or min value (number); inclusive. */\n min?: number\n /** Max length (text) or max value (number); inclusive. */\n max?: number\n /**\n * Branch override: given the answers so far, the id of the next question\n * (or null to end early). Omit for linear flow (next in declaration order).\n */\n next?(answers: IntakeAnswers): string | null\n}\n\n/** The intake definition: an ordered, addressable set of questions. */\nexport interface IntakeGraph {\n /** Stable id for the intake definition (e.g. 'user-onboarding-v1'). */\n id: string\n /** Human title shown at the top of the interview. */\n title: string\n /** Optional one-line description. */\n description?: string\n /** Questions in declaration (default traversal) order. */\n questions: IntakeQuestion[]\n}\n\n/** The reason an answer failed validation. */\nexport type AnswerRejectionReason =\n | 'required'\n | 'wrong-type'\n | 'too-short'\n | 'too-long'\n | 'too-small'\n | 'too-large'\n | 'not-an-option'\n | 'invalid-url'\n | 'invalid-email'\n | 'unknown-question'\n\n/** Describe validation outcome of an answer including success status and optional rejection reason */\nexport interface AnswerValidationResult {\n ok: boolean\n reason?: AnswerRejectionReason\n}\n\nconst URL_PATTERN = /^https?:\\/\\/[^\\s/$.?#].[^\\s]*$/i\nconst EMAIL_PATTERN = /^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/\n\n/** Look up a question by id, or null if the graph has none. */\nexport function getQuestion(graph: IntakeGraph, questionId: string): IntakeQuestion | null {\n return graph.questions.find((q) => q.id === questionId) ?? null\n}\n\n/** True when an answer value is present (not null/undefined/empty). */\nexport function hasAnswer(value: IntakeAnswerValue | undefined): boolean {\n if (value == null) return false\n if (typeof value === 'string') return value.trim().length > 0\n if (Array.isArray(value)) return value.length > 0\n return true\n}\n\n/**\n * Validate one answer against its question. Pure: no I/O, no graph mutation.\n * An empty value is rejected as `required` only when the question is required;\n * an empty value for an optional question is OK (the user skipped it).\n */\nexport function validateAnswer(\n question: IntakeQuestion,\n value: IntakeAnswerValue | undefined,\n): AnswerValidationResult {\n if (!hasAnswer(value)) {\n return question.required ? { ok: false, reason: 'required' } : { ok: true }\n }\n\n switch (question.type) {\n case 'text':\n case 'long-text': {\n if (typeof value !== 'string') return { ok: false, reason: 'wrong-type' }\n const len = value.trim().length\n if (question.min != null && len < question.min) return { ok: false, reason: 'too-short' }\n if (question.max != null && len > question.max) return { ok: false, reason: 'too-long' }\n return { ok: true }\n }\n case 'url': {\n if (typeof value !== 'string') return { ok: false, reason: 'wrong-type' }\n return URL_PATTERN.test(value.trim()) ? { ok: true } : { ok: false, reason: 'invalid-url' }\n }\n case 'email': {\n if (typeof value !== 'string') return { ok: false, reason: 'wrong-type' }\n return EMAIL_PATTERN.test(value.trim()) ? { ok: true } : { ok: false, reason: 'invalid-email' }\n }\n case 'number': {\n if (typeof value !== 'number' || !Number.isFinite(value)) return { ok: false, reason: 'wrong-type' }\n if (question.min != null && value < question.min) return { ok: false, reason: 'too-small' }\n if (question.max != null && value > question.max) return { ok: false, reason: 'too-large' }\n return { ok: true }\n }\n case 'boolean': {\n return typeof value === 'boolean' ? { ok: true } : { ok: false, reason: 'wrong-type' }\n }\n case 'single-select': {\n if (typeof value !== 'string') return { ok: false, reason: 'wrong-type' }\n return optionExists(question, value) ? { ok: true } : { ok: false, reason: 'not-an-option' }\n }\n case 'multi-select': {\n if (!Array.isArray(value)) return { ok: false, reason: 'wrong-type' }\n if (!value.every((v) => typeof v === 'string' && optionExists(question, v))) {\n return { ok: false, reason: 'not-an-option' }\n }\n if (question.min != null && value.length < question.min) return { ok: false, reason: 'too-short' }\n if (question.max != null && value.length > question.max) return { ok: false, reason: 'too-long' }\n return { ok: true }\n }\n default:\n return { ok: false, reason: 'wrong-type' }\n }\n}\n\n/**\n * The single question to ask next given the answers so far, or null when the\n * interview is done. Walks the graph from the first question, following each\n * question's `next` branch (or declaration order) and stopping at the first\n * reachable question that has no valid answer yet. Deterministic and pure.\n *\n * A `next` that points at a missing id, or returns null, ends the walk — so a\n * malformed graph terminates rather than looping. A visited-set guards against\n * a cyclic `next`.\n */\nexport function nextQuestion(graph: IntakeGraph, answers: IntakeAnswers): IntakeQuestion | null {\n if (graph.questions.length === 0) return null\n const visited = new Set<string>()\n let current: IntakeQuestion | null = graph.questions[0] ?? null\n\n while (current) {\n if (visited.has(current.id)) return null\n visited.add(current.id)\n\n const answer = answers[current.id]\n const validity = validateAnswer(current, answer)\n if (!validity.ok || (current.required && !hasAnswer(answer))) {\n return current\n }\n\n current = advance(graph, current, answers)\n }\n\n return null\n}\n\n/**\n * True when every REQUIRED question reachable under the current answers has a\n * valid answer. Pure: it replays the same traversal `nextQuestion` uses and is\n * complete exactly when that traversal has no question left to ask.\n */\nexport function isComplete(graph: IntakeGraph, answers: IntakeAnswers): boolean {\n return nextQuestion(graph, answers) === null\n}\n\n/**\n * Progress as answered-vs-total over the REACHABLE required questions under the\n * current answers — what a progress bar renders. Optional questions don't count\n * toward the denominator (they never block completion).\n */\nexport function intakeProgress(graph: IntakeGraph, answers: IntakeAnswers): { answered: number; total: number } {\n const reachable = reachableQuestions(graph, answers)\n const required = reachable.filter((q) => q.required)\n const answered = required.filter((q) => validateAnswer(q, answers[q.id]).ok && hasAnswer(answers[q.id])).length\n return { answered, total: required.length }\n}\n\n/** The questions reachable under the current answers, in traversal order. */\nexport function reachableQuestions(graph: IntakeGraph, answers: IntakeAnswers): IntakeQuestion[] {\n if (graph.questions.length === 0) return []\n const visited = new Set<string>()\n const out: IntakeQuestion[] = []\n let current: IntakeQuestion | null = graph.questions[0] ?? null\n\n while (current) {\n if (visited.has(current.id)) break\n visited.add(current.id)\n out.push(current)\n current = advance(graph, current, answers)\n }\n return out\n}\n\nfunction advance(graph: IntakeGraph, current: IntakeQuestion, answers: IntakeAnswers): IntakeQuestion | null {\n if (current.next) {\n const nextId = current.next(answers)\n return nextId == null ? null : getQuestion(graph, nextId)\n }\n const index = graph.questions.indexOf(current)\n return graph.questions[index + 1] ?? null\n}\n\nfunction optionExists(question: IntakeQuestion, value: string): boolean {\n return (question.options ?? []).some((option) => option.value === value)\n}\n","/**\n * Pure completion-state helpers for an intake payload — the small algebra over\n * the JSON blob the store persists. No I/O: the store reads/writes the row;\n * these functions only build and judge the payload shape, so the same logic\n * runs in the UI, the handlers, and the DB layer without divergence.\n *\n * A payload is `{ graphId, answers, completedAt? }`. It carries the answers\n * and the id of the graph they were collected against, so a later graph\n * revision can detect a stale payload (`graphId` mismatch) rather than\n * silently mixing answers from two question sets.\n */\n\nimport type { IntakeAnswers, IntakeAnswerValue, IntakeGraph } from './model'\nimport { isComplete } from './model'\n\n/** The persisted JSON for one intake (the `payload` column). */\nexport interface IntakePayload {\n /** The graph id the answers were collected against — guards stale schemas. */\n graphId: string\n answers: IntakeAnswers\n /** ISO-8601 instant the intake was completed, or undefined while in progress. */\n completedAt?: string\n}\n\n/** An empty payload for a fresh intake against `graph`. */\nexport function emptyPayload(graph: IntakeGraph): IntakePayload {\n return { graphId: graph.id, answers: {} }\n}\n\n/** A copy of `payload` with one answer set (does not mutate the input). */\nexport function withAnswer(\n payload: IntakePayload,\n questionId: string,\n value: IntakeAnswerValue,\n): IntakePayload {\n return { ...payload, answers: { ...payload.answers, [questionId]: value } }\n}\n\n/**\n * True when the payload's answers complete the graph AND the payload was\n * collected against THIS graph. A `graphId` mismatch is never \"complete\" —\n * the answers belong to a different question set and must be re-collected.\n */\nexport function payloadComplete(graph: IntakeGraph, payload: IntakePayload): boolean {\n if (payload.graphId !== graph.id) return false\n return isComplete(graph, payload.answers)\n}\n\n/** True when the payload was collected against a DIFFERENT graph revision. */\nexport function payloadIsStale(graph: IntakeGraph, payload: IntakePayload): boolean {\n return payload.graphId !== graph.id\n}\n\n/**\n * Stamp the payload complete at `at` (default now). Returns a copy; pure. The\n * caller has already checked `payloadComplete` — this only records the instant.\n */\nexport function markComplete(payload: IntakePayload, at: Date = new Date()): IntakePayload {\n return { ...payload, completedAt: at.toISOString() }\n}\n"],"mappings":"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","names":[]}
|
|
@@ -4,7 +4,7 @@ import {
|
|
|
4
4
|
} from "./chunk-I6BZVIRH.js";
|
|
5
5
|
import {
|
|
6
6
|
defineAppTool
|
|
7
|
-
} from "./chunk-
|
|
7
|
+
} from "./chunk-LJASCU5V.js";
|
|
8
8
|
import {
|
|
9
9
|
ToolInputError
|
|
10
10
|
} from "./chunk-E65YV7VV.js";
|
|
@@ -772,4 +772,4 @@ export {
|
|
|
772
772
|
PROFILE_MEMORY_PREFIX,
|
|
773
773
|
profileTools
|
|
774
774
|
};
|
|
775
|
-
//# sourceMappingURL=chunk-
|
|
775
|
+
//# sourceMappingURL=chunk-T3IO7QIW.js.map
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import {
|
|
2
|
+
ToolInputError,
|
|
3
|
+
validateOpenUIJsonNode
|
|
4
|
+
} from "./chunk-E65YV7VV.js";
|
|
5
|
+
|
|
6
|
+
// src/tools/render-ui-schema.ts
|
|
7
|
+
function parseRenderUiSchema(value) {
|
|
8
|
+
if (typeof value === "string") {
|
|
9
|
+
try {
|
|
10
|
+
value = JSON.parse(value);
|
|
11
|
+
} catch {
|
|
12
|
+
throw new ToolInputError("invalid_schema", "$: schema JSON text is malformed.");
|
|
13
|
+
}
|
|
14
|
+
if (typeof value === "string") {
|
|
15
|
+
throw new ToolInputError("invalid_schema", "$: schema must encode one OpenUI node object, not another JSON string.");
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
const result = validateOpenUIJsonNode(value);
|
|
19
|
+
if (!result.ok) {
|
|
20
|
+
throw new ToolInputError("invalid_schema", `${result.issue.path}: ${result.issue.message}`);
|
|
21
|
+
}
|
|
22
|
+
return result.value;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export {
|
|
26
|
+
parseRenderUiSchema
|
|
27
|
+
};
|
|
28
|
+
//# sourceMappingURL=chunk-WHK5CFDB.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/tools/render-ui-schema.ts"],"sourcesContent":["import { validateOpenUIJsonNode } from '@tangle-network/ui/openui-schema'\nimport { ToolInputError } from './errors'\n\n/** Decode one tool-transport JSON text, then enforce the renderer's node contract. */\nexport function parseRenderUiSchema(value: unknown) {\n if (typeof value === 'string') {\n try {\n value = JSON.parse(value) as unknown\n } catch {\n throw new ToolInputError('invalid_schema', '$: schema JSON text is malformed.')\n }\n if (typeof value === 'string') {\n throw new ToolInputError('invalid_schema', '$: schema must encode one OpenUI node object, not another JSON string.')\n }\n }\n const result = validateOpenUIJsonNode(value)\n if (!result.ok) {\n throw new ToolInputError('invalid_schema', `${result.issue.path}: ${result.issue.message}`)\n }\n return result.value\n}\n"],"mappings":";;;;;;AAIO,SAAS,oBAAoB,OAAgB;AAClD,MAAI,OAAO,UAAU,UAAU;AAC7B,QAAI;AACF,cAAQ,KAAK,MAAM,KAAK;AAAA,IAC1B,QAAQ;AACN,YAAM,IAAI,eAAe,kBAAkB,mCAAmC;AAAA,IAChF;AACA,QAAI,OAAO,UAAU,UAAU;AAC7B,YAAM,IAAI,eAAe,kBAAkB,wEAAwE;AAAA,IACrH;AAAA,EACF;AACA,QAAM,SAAS,uBAAuB,KAAK;AAC3C,MAAI,CAAC,OAAO,IAAI;AACd,UAAM,IAAI,eAAe,kBAAkB,GAAG,OAAO,MAAM,IAAI,KAAK,OAAO,MAAM,OAAO,EAAE;AAAA,EAC5F;AACA,SAAO,OAAO;AAChB;","names":[]}
|
package/dist/intakes/drizzle.js
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
2
|
+
emptyPayload,
|
|
3
|
+
getQuestion,
|
|
4
|
+
markComplete,
|
|
5
|
+
payloadComplete,
|
|
6
|
+
validateAnswer,
|
|
7
|
+
withAnswer
|
|
8
|
+
} from "../chunk-LRW36WCF.js";
|
|
8
9
|
|
|
9
10
|
// src/intakes/drizzle/schema.ts
|
|
10
11
|
import { sql } from "drizzle-orm";
|
|
@@ -52,6 +53,113 @@ function createIntakeTables(opts) {
|
|
|
52
53
|
}
|
|
53
54
|
return result;
|
|
54
55
|
}
|
|
56
|
+
|
|
57
|
+
// src/intakes/drizzle/store.ts
|
|
58
|
+
import { eq } from "drizzle-orm";
|
|
59
|
+
var IntakeError = class extends Error {
|
|
60
|
+
code;
|
|
61
|
+
constructor(code, message) {
|
|
62
|
+
super(message);
|
|
63
|
+
this.name = "IntakeError";
|
|
64
|
+
this.code = code;
|
|
65
|
+
}
|
|
66
|
+
};
|
|
67
|
+
function createScopedStore(opts) {
|
|
68
|
+
const { db, graph, table, scopeColumn, scopeValue, insertScope } = opts;
|
|
69
|
+
async function loadRow() {
|
|
70
|
+
const [row] = await db.select().from(table).where(eq(scopeColumn, scopeValue)).limit(1);
|
|
71
|
+
return row ?? null;
|
|
72
|
+
}
|
|
73
|
+
function toState(row) {
|
|
74
|
+
if (!row) {
|
|
75
|
+
return { payload: emptyPayload(graph), completed: false, completedAt: null };
|
|
76
|
+
}
|
|
77
|
+
const payload = normalizePayload(row.payload, row.graphId);
|
|
78
|
+
const completedAt = toDate(row.completedAt);
|
|
79
|
+
return {
|
|
80
|
+
payload,
|
|
81
|
+
completed: completedAt != null && payloadComplete(graph, payload),
|
|
82
|
+
completedAt
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
async function get() {
|
|
86
|
+
return toState(await loadRow());
|
|
87
|
+
}
|
|
88
|
+
async function upsertPayload(payload, completedAt) {
|
|
89
|
+
const existing = await loadRow();
|
|
90
|
+
if (existing) {
|
|
91
|
+
await db.update(table).set({ graphId: payload.graphId, payload, completedAt, updatedAt: /* @__PURE__ */ new Date() }).where(eq(scopeColumn, scopeValue));
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
await db.insert(table).values({
|
|
95
|
+
...insertScope,
|
|
96
|
+
graphId: payload.graphId,
|
|
97
|
+
payload,
|
|
98
|
+
completedAt
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
async function save(questionId, value) {
|
|
102
|
+
const question = getQuestion(graph, questionId);
|
|
103
|
+
if (!question) throw new IntakeError("unknown-question", `No question '${questionId}' in intake '${graph.id}'`);
|
|
104
|
+
const validity = validateAnswer(question, value);
|
|
105
|
+
if (!validity.ok) {
|
|
106
|
+
throw new IntakeError("invalid-answer", `Answer to '${questionId}' rejected: ${validity.reason}`);
|
|
107
|
+
}
|
|
108
|
+
const current = await get();
|
|
109
|
+
const next = withAnswer(current.payload, questionId, value);
|
|
110
|
+
await upsertPayload(next, current.completedAt);
|
|
111
|
+
return toState({ graphId: next.graphId, payload: next, completedAt: current.completedAt });
|
|
112
|
+
}
|
|
113
|
+
async function complete() {
|
|
114
|
+
const current = await get();
|
|
115
|
+
if (current.payload.graphId !== graph.id) {
|
|
116
|
+
throw new IntakeError("stale-graph", `Intake payload was collected against '${current.payload.graphId}', not '${graph.id}'`);
|
|
117
|
+
}
|
|
118
|
+
if (!payloadComplete(graph, current.payload)) {
|
|
119
|
+
throw new IntakeError("incomplete", `Intake '${graph.id}' has unanswered required questions`);
|
|
120
|
+
}
|
|
121
|
+
const completedPayload = markComplete(current.payload);
|
|
122
|
+
const completedAt = new Date(completedPayload.completedAt);
|
|
123
|
+
await upsertPayload(completedPayload, completedAt);
|
|
124
|
+
return { payload: completedPayload, completed: true, completedAt };
|
|
125
|
+
}
|
|
126
|
+
return { get, save, complete };
|
|
127
|
+
}
|
|
128
|
+
function createUserIntakeStore(opts) {
|
|
129
|
+
return createScopedStore({
|
|
130
|
+
db: opts.db,
|
|
131
|
+
graph: opts.graph,
|
|
132
|
+
table: opts.table,
|
|
133
|
+
scopeColumn: opts.table.userId,
|
|
134
|
+
scopeValue: opts.userId,
|
|
135
|
+
insertScope: { userId: opts.userId }
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
function createProjectIntakeStore(opts) {
|
|
139
|
+
return createScopedStore({
|
|
140
|
+
db: opts.db,
|
|
141
|
+
graph: opts.graph,
|
|
142
|
+
table: opts.table,
|
|
143
|
+
scopeColumn: opts.table.workspaceId,
|
|
144
|
+
scopeValue: opts.workspaceId,
|
|
145
|
+
insertScope: { workspaceId: opts.workspaceId }
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
function normalizePayload(raw, graphId) {
|
|
149
|
+
if (raw && typeof raw === "object" && "answers" in raw) {
|
|
150
|
+
const candidate = raw;
|
|
151
|
+
return {
|
|
152
|
+
graphId: candidate.graphId ?? graphId,
|
|
153
|
+
answers: candidate.answers ?? {},
|
|
154
|
+
...candidate.completedAt ? { completedAt: candidate.completedAt } : {}
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
return { graphId, answers: {} };
|
|
158
|
+
}
|
|
159
|
+
function toDate(value) {
|
|
160
|
+
if (value == null) return null;
|
|
161
|
+
return value instanceof Date ? value : new Date(Number(value) * (Number(value) < 1e12 ? 1e3 : 1));
|
|
162
|
+
}
|
|
55
163
|
export {
|
|
56
164
|
IntakeError,
|
|
57
165
|
createIntakeTables,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/intakes/drizzle/schema.ts"],"sourcesContent":["/**\n * Drizzle schema factory for the intake tables. The product owns the user (and\n * optionally workspace) tables; this factory creates the intake tables and\n * wires their foreign keys into the passed-in tables so the whole graph lives\n * in one drizzle schema with real cascade semantics.\n *\n * Two tables, two scopes:\n * - `user_intake` — the one-time onboarding interview, keyed on `user.id`\n * (UNIQUE per user: one onboarding payload per user). ALWAYS created.\n * - `project_intake` — the structured intake attached to a workspace, keyed\n * on `workspace.id` (UNIQUE per workspace). Created ONLY when a\n * `workspaceTable` is passed.\n *\n * `workspaceTable` is OPTIONAL by design: a non-workspace app (a single-user\n * tool, tax-without-workspaces) adopts per-user onboarding alone with zero\n * teams and zero workspace concept. When omitted, `createIntakeTables` returns\n * `{ userIntake }` and no `projectIntake` — the FK to a workspace table that\n * does not exist is never created.\n *\n * Imports `drizzle-orm` at module top — that is WHY this lives behind the\n * `/intakes/drizzle` sub-subpath. The pure `./intakes` leaf imports none of\n * this, so a consumer that never touches the DB never pulls the optional peer.\n */\n\nimport { sql } from 'drizzle-orm'\nimport { index, integer, sqliteTable, text, uniqueIndex } from 'drizzle-orm/sqlite-core'\nimport type { AnySQLiteColumn, AnySQLiteTable } from 'drizzle-orm/sqlite-core'\n\n/** A product table referenced by FK — only the `id` column is touched. */\nexport type IntakeParentTable = AnySQLiteTable & { id: AnySQLiteColumn }\n\n/** Define options for creating intake tables including user and optional workspace references */\nexport interface CreateIntakeTablesOptions {\n /** The product's user table — user-intake rows reference `userTable.id`. */\n userTable: IntakeParentTable\n /**\n * The product's workspace table — project-intake rows reference\n * `workspaceTable.id`. OPTIONAL: omit it for a single-user / non-workspace\n * app that wants per-user onboarding only. When omitted, no `projectIntake`\n * table is created.\n */\n workspaceTable?: IntakeParentTable\n}\n\nconst hexId = () => text('id').primaryKey().default(sql`(lower(hex(randomblob(16))))`)\n\nconst createdAt = () => integer('created_at', { mode: 'timestamp' }).notNull().default(sql`(unixepoch())`)\n\nconst updatedAt = () => integer('updated_at', { mode: 'timestamp' }).notNull().default(sql`(unixepoch())`)\n\nfunction createUserIntakeTable(userTable: IntakeParentTable) {\n return sqliteTable('user_intake', {\n id: hexId(),\n userId: text('user_id').notNull().references(() => userTable.id, { onDelete: 'cascade' }),\n /** The intake definition id the payload was collected against. */\n graphId: text('graph_id').notNull(),\n /** The IntakePayload JSON blob. */\n payload: text('payload', { mode: 'json' }).notNull(),\n /** Set when the user finishes the onboarding interview; null while open. */\n completedAt: integer('completed_at', { mode: 'timestamp' }),\n createdAt: createdAt(),\n updatedAt: updatedAt(),\n }, (table) => [\n uniqueIndex('uniq_user_intake_user').on(table.userId),\n ])\n}\n\nfunction createProjectIntakeTable(workspaceTable: IntakeParentTable) {\n return sqliteTable('project_intake', {\n id: hexId(),\n workspaceId: text('workspace_id').notNull().references(() => workspaceTable.id, { onDelete: 'cascade' }),\n graphId: text('graph_id').notNull(),\n payload: text('payload', { mode: 'json' }).notNull(),\n completedAt: integer('completed_at', { mode: 'timestamp' }),\n createdAt: createdAt(),\n updatedAt: updatedAt(),\n }, (table) => [\n uniqueIndex('uniq_project_intake_workspace').on(table.workspaceId),\n index('idx_project_intake_graph').on(table.graphId),\n ])\n}\n\n/**\n * Build the intake tables wired to the product's tables. Always returns\n * `userIntake`; returns `projectIntake` only when `workspaceTable` is passed.\n * The return type carries `projectIntake?` so a consumer that passes no\n * workspace table cannot reference a table that does not exist.\n */\nexport function createIntakeTables<O extends CreateIntakeTablesOptions>(\n opts: O,\n): IntakeTables<O> {\n const userIntake = createUserIntakeTable(opts.userTable)\n const result: { userIntake: ReturnType<typeof createUserIntakeTable>; projectIntake?: ReturnType<typeof createProjectIntakeTable> } = {\n userIntake,\n }\n if (opts.workspaceTable) {\n result.projectIntake = createProjectIntakeTable(opts.workspaceTable)\n }\n return result as IntakeTables<O>\n}\n\n/** Resolve the structure and data of a user intake table based on the createUserIntakeTable function */\nexport type UserIntakeTable = ReturnType<typeof createUserIntakeTable>\n/** Resolve the structure and data of the project intake table from the factory function */\nexport type ProjectIntakeTable = ReturnType<typeof createProjectIntakeTable>\n\n/**\n * The tables returned for a given options shape: `projectIntake` is present in\n * the type exactly when `workspaceTable` was provided.\n */\nexport type IntakeTables<O extends CreateIntakeTablesOptions> =\n O extends { workspaceTable: IntakeParentTable }\n ? { userIntake: UserIntakeTable; projectIntake: ProjectIntakeTable }\n : { userIntake: UserIntakeTable; projectIntake?: ProjectIntakeTable }\n\n/** The union table shape, for code that handles either scope generically. */\nexport type AnyIntakeTables = { userIntake: UserIntakeTable; projectIntake?: ProjectIntakeTable }\n\n/** Infer and represent a selected row from the UserIntakeTable data structure */\nexport type UserIntakeRow = UserIntakeTable['$inferSelect']\n/** Resolve the selected data structure for a project intake table row */\nexport type ProjectIntakeRow = ProjectIntakeTable['$inferSelect']\n"],"mappings":";;;;;;;;;AAwBA,SAAS,WAAW;AACpB,SAAS,OAAO,SAAS,aAAa,MAAM,mBAAmB;AAmB/D,IAAM,QAAQ,MAAM,KAAK,IAAI,EAAE,WAAW,EAAE,QAAQ,iCAAiC;AAErF,IAAM,YAAY,MAAM,QAAQ,cAAc,EAAE,MAAM,YAAY,CAAC,EAAE,QAAQ,EAAE,QAAQ,kBAAkB;AAEzG,IAAM,YAAY,MAAM,QAAQ,cAAc,EAAE,MAAM,YAAY,CAAC,EAAE,QAAQ,EAAE,QAAQ,kBAAkB;AAEzG,SAAS,sBAAsB,WAA8B;AAC3D,SAAO,YAAY,eAAe;AAAA,IAChC,IAAI,MAAM;AAAA,IACV,QAAQ,KAAK,SAAS,EAAE,QAAQ,EAAE,WAAW,MAAM,UAAU,IAAI,EAAE,UAAU,UAAU,CAAC;AAAA;AAAA,IAExF,SAAS,KAAK,UAAU,EAAE,QAAQ;AAAA;AAAA,IAElC,SAAS,KAAK,WAAW,EAAE,MAAM,OAAO,CAAC,EAAE,QAAQ;AAAA;AAAA,IAEnD,aAAa,QAAQ,gBAAgB,EAAE,MAAM,YAAY,CAAC;AAAA,IAC1D,WAAW,UAAU;AAAA,IACrB,WAAW,UAAU;AAAA,EACvB,GAAG,CAAC,UAAU;AAAA,IACZ,YAAY,uBAAuB,EAAE,GAAG,MAAM,MAAM;AAAA,EACtD,CAAC;AACH;AAEA,SAAS,yBAAyB,gBAAmC;AACnE,SAAO,YAAY,kBAAkB;AAAA,IACnC,IAAI,MAAM;AAAA,IACV,aAAa,KAAK,cAAc,EAAE,QAAQ,EAAE,WAAW,MAAM,eAAe,IAAI,EAAE,UAAU,UAAU,CAAC;AAAA,IACvG,SAAS,KAAK,UAAU,EAAE,QAAQ;AAAA,IAClC,SAAS,KAAK,WAAW,EAAE,MAAM,OAAO,CAAC,EAAE,QAAQ;AAAA,IACnD,aAAa,QAAQ,gBAAgB,EAAE,MAAM,YAAY,CAAC;AAAA,IAC1D,WAAW,UAAU;AAAA,IACrB,WAAW,UAAU;AAAA,EACvB,GAAG,CAAC,UAAU;AAAA,IACZ,YAAY,+BAA+B,EAAE,GAAG,MAAM,WAAW;AAAA,IACjE,MAAM,0BAA0B,EAAE,GAAG,MAAM,OAAO;AAAA,EACpD,CAAC;AACH;AAQO,SAAS,mBACd,MACiB;AACjB,QAAM,aAAa,sBAAsB,KAAK,SAAS;AACvD,QAAM,SAAgI;AAAA,IACpI;AAAA,EACF;AACA,MAAI,KAAK,gBAAgB;AACvB,WAAO,gBAAgB,yBAAyB,KAAK,cAAc;AAAA,EACrE;AACA,SAAO;AACT;","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../src/intakes/drizzle/schema.ts","../../src/intakes/drizzle/store.ts"],"sourcesContent":["/**\n * Drizzle schema factory for the intake tables. The product owns the user (and\n * optionally workspace) tables; this factory creates the intake tables and\n * wires their foreign keys into the passed-in tables so the whole graph lives\n * in one drizzle schema with real cascade semantics.\n *\n * Two tables, two scopes:\n * - `user_intake` — the one-time onboarding interview, keyed on `user.id`\n * (UNIQUE per user: one onboarding payload per user). ALWAYS created.\n * - `project_intake` — the structured intake attached to a workspace, keyed\n * on `workspace.id` (UNIQUE per workspace). Created ONLY when a\n * `workspaceTable` is passed.\n *\n * `workspaceTable` is OPTIONAL by design: a non-workspace app (a single-user\n * tool, tax-without-workspaces) adopts per-user onboarding alone with zero\n * teams and zero workspace concept. When omitted, `createIntakeTables` returns\n * `{ userIntake }` and no `projectIntake` — the FK to a workspace table that\n * does not exist is never created.\n *\n * Imports `drizzle-orm` at module top — that is WHY this lives behind the\n * `/intakes/drizzle` sub-subpath. The pure `./intakes` leaf imports none of\n * this, so a consumer that never touches the DB never pulls the optional peer.\n */\n\nimport { sql } from 'drizzle-orm'\nimport { index, integer, sqliteTable, text, uniqueIndex } from 'drizzle-orm/sqlite-core'\nimport type { AnySQLiteColumn, AnySQLiteTable } from 'drizzle-orm/sqlite-core'\n\n/** A product table referenced by FK — only the `id` column is touched. */\nexport type IntakeParentTable = AnySQLiteTable & { id: AnySQLiteColumn }\n\n/** Define options for creating intake tables including user and optional workspace references */\nexport interface CreateIntakeTablesOptions {\n /** The product's user table — user-intake rows reference `userTable.id`. */\n userTable: IntakeParentTable\n /**\n * The product's workspace table — project-intake rows reference\n * `workspaceTable.id`. OPTIONAL: omit it for a single-user / non-workspace\n * app that wants per-user onboarding only. When omitted, no `projectIntake`\n * table is created.\n */\n workspaceTable?: IntakeParentTable\n}\n\nconst hexId = () => text('id').primaryKey().default(sql`(lower(hex(randomblob(16))))`)\n\nconst createdAt = () => integer('created_at', { mode: 'timestamp' }).notNull().default(sql`(unixepoch())`)\n\nconst updatedAt = () => integer('updated_at', { mode: 'timestamp' }).notNull().default(sql`(unixepoch())`)\n\nfunction createUserIntakeTable(userTable: IntakeParentTable) {\n return sqliteTable('user_intake', {\n id: hexId(),\n userId: text('user_id').notNull().references(() => userTable.id, { onDelete: 'cascade' }),\n /** The intake definition id the payload was collected against. */\n graphId: text('graph_id').notNull(),\n /** The IntakePayload JSON blob. */\n payload: text('payload', { mode: 'json' }).notNull(),\n /** Set when the user finishes the onboarding interview; null while open. */\n completedAt: integer('completed_at', { mode: 'timestamp' }),\n createdAt: createdAt(),\n updatedAt: updatedAt(),\n }, (table) => [\n uniqueIndex('uniq_user_intake_user').on(table.userId),\n ])\n}\n\nfunction createProjectIntakeTable(workspaceTable: IntakeParentTable) {\n return sqliteTable('project_intake', {\n id: hexId(),\n workspaceId: text('workspace_id').notNull().references(() => workspaceTable.id, { onDelete: 'cascade' }),\n graphId: text('graph_id').notNull(),\n payload: text('payload', { mode: 'json' }).notNull(),\n completedAt: integer('completed_at', { mode: 'timestamp' }),\n createdAt: createdAt(),\n updatedAt: updatedAt(),\n }, (table) => [\n uniqueIndex('uniq_project_intake_workspace').on(table.workspaceId),\n index('idx_project_intake_graph').on(table.graphId),\n ])\n}\n\n/**\n * Build the intake tables wired to the product's tables. Always returns\n * `userIntake`; returns `projectIntake` only when `workspaceTable` is passed.\n * The return type carries `projectIntake?` so a consumer that passes no\n * workspace table cannot reference a table that does not exist.\n */\nexport function createIntakeTables<O extends CreateIntakeTablesOptions>(\n opts: O,\n): IntakeTables<O> {\n const userIntake = createUserIntakeTable(opts.userTable)\n const result: { userIntake: ReturnType<typeof createUserIntakeTable>; projectIntake?: ReturnType<typeof createProjectIntakeTable> } = {\n userIntake,\n }\n if (opts.workspaceTable) {\n result.projectIntake = createProjectIntakeTable(opts.workspaceTable)\n }\n return result as IntakeTables<O>\n}\n\n/** Resolve the structure and data of a user intake table based on the createUserIntakeTable function */\nexport type UserIntakeTable = ReturnType<typeof createUserIntakeTable>\n/** Resolve the structure and data of the project intake table from the factory function */\nexport type ProjectIntakeTable = ReturnType<typeof createProjectIntakeTable>\n\n/**\n * The tables returned for a given options shape: `projectIntake` is present in\n * the type exactly when `workspaceTable` was provided.\n */\nexport type IntakeTables<O extends CreateIntakeTablesOptions> =\n O extends { workspaceTable: IntakeParentTable }\n ? { userIntake: UserIntakeTable; projectIntake: ProjectIntakeTable }\n : { userIntake: UserIntakeTable; projectIntake?: ProjectIntakeTable }\n\n/** The union table shape, for code that handles either scope generically. */\nexport type AnyIntakeTables = { userIntake: UserIntakeTable; projectIntake?: ProjectIntakeTable }\n\n/** Infer and represent a selected row from the UserIntakeTable data structure */\nexport type UserIntakeRow = UserIntakeTable['$inferSelect']\n/** Resolve the selected data structure for a project intake table row */\nexport type ProjectIntakeRow = ProjectIntakeTable['$inferSelect']\n","/**\n * Drizzle-backed store over the tables from `createIntakeTables`. One store\n * builder per scope: `createUserIntakeStore` (keyed on userId) and\n * `createProjectIntakeStore` (keyed on workspaceId). Each returns the same\n * three-method surface — `get` / `save` / `complete` — so the api handlers and\n * the UI talk to one shape regardless of scope.\n *\n * Works against any SQLite drizzle driver (better-sqlite3, D1, libsql): the\n * builders are awaited, never `.run()`/`.all()`, so sync and async drivers\n * behave identically.\n *\n * Validation is fail-loud and pure (from the `./intakes` leaf). `save` runs\n * `validateAnswer` before writing, so an invalid answer can never enter the\n * payload; `complete` runs `payloadComplete` and refuses to stamp an\n * incomplete (or stale-graph) intake — it throws a typed `IntakeError` rather\n * than silently writing a half-finished onboarding state.\n *\n * `getProjectIntakeStore` pins `workspaceId` in every WHERE clause; RBAC runs\n * in the route before the store is built, but the scope key is enforced here\n * too, so a leaked id can never read or write across the boundary.\n *\n * Imports `drizzle-orm`, so this is a subpath, never re-exported from root.\n */\n\nimport { eq } from 'drizzle-orm'\nimport type { BaseSQLiteDatabase } from 'drizzle-orm/sqlite-core'\nimport {\n type IntakeAnswerValue,\n type IntakeGraph,\n getQuestion,\n validateAnswer,\n} from '../model'\nimport {\n type IntakePayload,\n emptyPayload,\n markComplete,\n payloadComplete,\n withAnswer,\n} from '../completion'\nimport type { ProjectIntakeTable, UserIntakeTable } from './schema'\n\n/** Any SQLite drizzle database — `any` erases driver-specific generics so\n * better-sqlite3, D1, and libsql handles all fit. */\nexport type IntakeDatabase = BaseSQLiteDatabase<'sync' | 'async', any, any>\n\n/** A loaded intake: the payload plus whether it is complete against the graph. */\nexport interface IntakeState {\n payload: IntakePayload\n completed: boolean\n completedAt: Date | null\n}\n\n/** Define error codes representing specific intake validation failures */\nexport type IntakeErrorCode = 'invalid-answer' | 'unknown-question' | 'incomplete' | 'stale-graph'\n\n/** Thrown by store mutations on a refused write — callers map it to a 4xx. */\nexport class IntakeError extends Error {\n readonly code: IntakeErrorCode\n constructor(code: IntakeErrorCode, message: string) {\n super(message)\n this.name = 'IntakeError'\n this.code = code\n }\n}\n\n/** The three-method store surface, identical for both scopes. */\nexport interface IntakeStore {\n /** Load the current intake, or seed an empty payload when none exists yet. */\n get(): Promise<IntakeState>\n /** Validate and persist one answer; returns the updated state. */\n save(questionId: string, value: IntakeAnswerValue): Promise<IntakeState>\n /** Stamp the intake complete; throws IntakeError when not yet completable. */\n complete(): Promise<IntakeState>\n}\n\ninterface IntakeTableShape {\n id: any\n graphId: any\n payload: any\n completedAt: any\n updatedAt: any\n}\n\ninterface ScopedStoreOptions {\n db: IntakeDatabase\n graph: IntakeGraph\n table: IntakeTableShape\n /** The scope column (userIntake.userId or projectIntake.workspaceId). */\n scopeColumn: any\n scopeValue: string\n /** Extra columns to set on insert (the scope key). */\n insertScope: Record<string, unknown>\n}\n\nfunction createScopedStore(opts: ScopedStoreOptions): IntakeStore {\n const { db, graph, table, scopeColumn, scopeValue, insertScope } = opts\n\n async function loadRow() {\n const [row] = await db\n .select()\n .from(table as any)\n .where(eq(scopeColumn, scopeValue))\n .limit(1)\n return (row ?? null) as\n | { id: string; graphId: string; payload: unknown; completedAt: Date | number | null }\n | null\n }\n\n function toState(\n row: { graphId: string; payload: unknown; completedAt: Date | number | null } | null,\n ): IntakeState {\n if (!row) {\n return { payload: emptyPayload(graph), completed: false, completedAt: null }\n }\n const payload = normalizePayload(row.payload, row.graphId)\n const completedAt = toDate(row.completedAt)\n return {\n payload,\n completed: completedAt != null && payloadComplete(graph, payload),\n completedAt,\n }\n }\n\n async function get(): Promise<IntakeState> {\n return toState(await loadRow())\n }\n\n async function upsertPayload(payload: IntakePayload, completedAt: Date | null) {\n const existing = await loadRow()\n if (existing) {\n await db\n .update(table as any)\n .set({ graphId: payload.graphId, payload, completedAt, updatedAt: new Date() })\n .where(eq(scopeColumn, scopeValue))\n return\n }\n await db.insert(table as any).values({\n ...insertScope,\n graphId: payload.graphId,\n payload,\n completedAt,\n })\n }\n\n async function save(questionId: string, value: IntakeAnswerValue): Promise<IntakeState> {\n const question = getQuestion(graph, questionId)\n if (!question) throw new IntakeError('unknown-question', `No question '${questionId}' in intake '${graph.id}'`)\n const validity = validateAnswer(question, value)\n if (!validity.ok) {\n throw new IntakeError('invalid-answer', `Answer to '${questionId}' rejected: ${validity.reason}`)\n }\n\n const current = await get()\n const next = withAnswer(current.payload, questionId, value)\n await upsertPayload(next, current.completedAt)\n return toState({ graphId: next.graphId, payload: next, completedAt: current.completedAt })\n }\n\n async function complete(): Promise<IntakeState> {\n const current = await get()\n if (current.payload.graphId !== graph.id) {\n throw new IntakeError('stale-graph', `Intake payload was collected against '${current.payload.graphId}', not '${graph.id}'`)\n }\n if (!payloadComplete(graph, current.payload)) {\n throw new IntakeError('incomplete', `Intake '${graph.id}' has unanswered required questions`)\n }\n const completedPayload = markComplete(current.payload)\n const completedAt = new Date(completedPayload.completedAt!)\n await upsertPayload(completedPayload, completedAt)\n return { payload: completedPayload, completed: true, completedAt }\n }\n\n return { get, save, complete }\n}\n\n/** Define options required to create a user intake store for onboarding data collection */\nexport interface CreateUserIntakeStoreOptions {\n db: IntakeDatabase\n /** The user-intake table from createIntakeTables. */\n table: UserIntakeTable\n /** The intake definition this store collects answers against. */\n graph: IntakeGraph\n /** The user whose onboarding this is. */\n userId: string\n}\n\n/** Build the per-user onboarding store, scoped to one userId. */\nexport function createUserIntakeStore(opts: CreateUserIntakeStoreOptions): IntakeStore {\n return createScopedStore({\n db: opts.db,\n graph: opts.graph,\n table: opts.table as unknown as IntakeTableShape,\n scopeColumn: opts.table.userId,\n scopeValue: opts.userId,\n insertScope: { userId: opts.userId },\n })\n}\n\n/** Define options required to create a project intake store including database, table, graph, and workspace ID */\nexport interface CreateProjectIntakeStoreOptions {\n db: IntakeDatabase\n /** The project-intake table from createIntakeTables (workspace scope). */\n table: ProjectIntakeTable\n graph: IntakeGraph\n /** The workspace this intake is attached to. */\n workspaceId: string\n}\n\n/** Build the per-project store, scoped to one workspaceId. */\nexport function createProjectIntakeStore(opts: CreateProjectIntakeStoreOptions): IntakeStore {\n return createScopedStore({\n db: opts.db,\n graph: opts.graph,\n table: opts.table as unknown as IntakeTableShape,\n scopeColumn: opts.table.workspaceId,\n scopeValue: opts.workspaceId,\n insertScope: { workspaceId: opts.workspaceId },\n })\n}\n\nfunction normalizePayload(raw: unknown, graphId: string): IntakePayload {\n if (raw && typeof raw === 'object' && 'answers' in (raw as Record<string, unknown>)) {\n const candidate = raw as Partial<IntakePayload>\n return {\n graphId: candidate.graphId ?? graphId,\n answers: candidate.answers ?? {},\n ...(candidate.completedAt ? { completedAt: candidate.completedAt } : {}),\n }\n }\n return { graphId, answers: {} }\n}\n\nfunction toDate(value: Date | number | null | undefined): Date | null {\n if (value == null) return null\n return value instanceof Date ? value : new Date(Number(value) * (Number(value) < 1e12 ? 1000 : 1))\n}\n"],"mappings":"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","names":[]}
|
package/dist/intakes/index.d.ts
CHANGED
|
@@ -15,13 +15,9 @@
|
|
|
15
15
|
* - `@tangle-network/agent-app/intakes/drizzle` — table factory
|
|
16
16
|
* (`createIntakeTables`, `workspaceTable` OPTIONAL) + store (imports
|
|
17
17
|
* `drizzle-orm`).
|
|
18
|
-
* - `@tangle-network/agent-app/intakes/api` — framework-neutral
|
|
19
|
-
* get-current / save-answer / complete handlers (imports `drizzle-orm`).
|
|
20
|
-
* - `@tangle-network/agent-app/intakes-react` — one-question-at-a-time
|
|
21
|
-
* interview UI (optional `react` peer).
|
|
22
18
|
*
|
|
23
|
-
* NEVER re-exported from the package root: `drizzle-orm`
|
|
24
|
-
*
|
|
19
|
+
* NEVER re-exported from the package root: `drizzle-orm` is an optional peer,
|
|
20
|
+
* and the whole point is that the tax stays clean.
|
|
25
21
|
*/
|
|
26
22
|
export * from './model';
|
|
27
23
|
export * from './completion';
|
package/dist/intakes/index.js
CHANGED
|
@@ -1,19 +1,17 @@
|
|
|
1
1
|
import {
|
|
2
2
|
emptyPayload,
|
|
3
|
-
markComplete,
|
|
4
|
-
payloadComplete,
|
|
5
|
-
payloadIsStale,
|
|
6
|
-
withAnswer
|
|
7
|
-
} from "../chunk-YZ6KQULN.js";
|
|
8
|
-
import {
|
|
9
3
|
getQuestion,
|
|
10
4
|
hasAnswer,
|
|
11
5
|
intakeProgress,
|
|
12
6
|
isComplete,
|
|
7
|
+
markComplete,
|
|
13
8
|
nextQuestion,
|
|
9
|
+
payloadComplete,
|
|
10
|
+
payloadIsStale,
|
|
14
11
|
reachableQuestions,
|
|
15
|
-
validateAnswer
|
|
16
|
-
|
|
12
|
+
validateAnswer,
|
|
13
|
+
withAnswer
|
|
14
|
+
} from "../chunk-LRW36WCF.js";
|
|
17
15
|
|
|
18
16
|
// src/intakes/context-sufficiency.ts
|
|
19
17
|
function presentValue(value) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/intakes/context-sufficiency.ts"],"sourcesContent":["/**\n * Pure context-sufficiency floor + conversational-gather scaffold — the\n * product-agnostic core of a CONVERSATIONAL intake. Where the question-graph\n * model (`./model`) runs a one-question-at-a-time interview, this leaf judges\n * whether an agent has gathered ENOUGH context to act, and renders the prompt\n * directive that tells the agent to fold the remaining gaps into its work\n * instead of running a form.\n *\n * Zero dependencies: no drizzle, no env, no react, no I/O, no product reads.\n * A product declares WHICH facts matter (`ContextFactSpec`), and its own\n * adapter resolves those facts + named substrate flags from whatever substrate\n * it owns (`ResolvedContextSignals`); this leaf only does the deterministic\n * combine and the wording. That separation is what makes the framework opt-in\n * and tree-shakeable, and what lets gtm/tax/legal/insurance share one core.\n *\n * Readiness is a two-part floor: SCOPE (every required fact has a value) AND\n * SUBSTRATE (at least one named substrate flag is true). Scope alone is not\n * enough — knowing the goal without any durable thing to act on is still\n * not-ready; substrate alone is not enough either. The product's adapter\n * decides what counts as a fact and what counts as substrate.\n */\n\n/** One fact the product treats as known context once it has a value. */\nexport interface ContextFact {\n /** Stable key the resolved-signals map is keyed on. */\n key: string\n /** Human label shown in the prompt's known/missing lists. */\n label: string\n /** When true, this fact must have a value for SCOPE to be met. */\n required?: boolean\n /** How the agent should gather this fact conversationally, if missing. */\n gatherHint?: string\n}\n\n/**\n * The product's declaration of what context matters: the facts that make up\n * scope, plus optional tool hints appended verbatim to the gather prompt (e.g.\n * a product passes the command that extracts a brand from a URL). Pure data —\n * the product supplies labels, hints, and tool lines; the framework never\n * names a product-specific concept itself.\n */\nexport interface ContextFactSpec {\n facts: ContextFact[]\n /** Lines appended after the gather directive — e.g. product tool pointers. */\n toolHints?: string[]\n}\n\n/**\n * What a product's adapter resolves from its own substrate, ready to combine.\n * `facts` carries the resolved VALUE per fact key (undefined/empty = not\n * known); `substrate` carries named boolean flags (e.g.\n * `{ brandConfirmed, configHasContext, coreKnowledgePresent }`) — any one true\n * satisfies the substrate half of the floor.\n */\nexport interface ResolvedContextSignals {\n facts: Record<string, string | undefined>\n substrate: Record<string, boolean>\n}\n\n/** A resolved fact that has a value — surfaced to the prompt as known context. */\nexport interface KnownFact {\n key: string\n label: string\n value: string\n}\n\n/** A required fact with no value — surfaced to the prompt as a gap to close. */\nexport interface MissingFact {\n key: string\n label: string\n gatherHint?: string\n}\n\n/** The deterministic verdict over the resolved signals. */\nexport interface ContextSufficiency {\n /** True when the floor is met: `hasScope && hasSubstrate`. */\n ready: boolean\n /** Every REQUIRED fact has a non-empty value. */\n hasScope: boolean\n /** At least one named substrate flag is true. */\n hasSubstrate: boolean\n /** Facts (required or not) that have a value. */\n knownFacts: KnownFact[]\n /** Required facts that have no value yet. */\n missingFacts: MissingFact[]\n /** The substrate flags as resolved, passed through for the prompt/caller. */\n substrate: Record<string, boolean>\n}\n\n/** Trim a fact value to a present string, or undefined when blank/absent. */\nfunction presentValue(value: string | undefined): string | undefined {\n if (typeof value !== 'string') return undefined\n const trimmed = value.trim()\n return trimmed.length > 0 ? trimmed : undefined\n}\n\n/**\n * Combine a fact spec with resolved signals into the readiness verdict. Pure,\n * deterministic, never throws — a missing fact key or an empty substrate map\n * reads as not-ready, not an error, so a fresh scope is honestly not-ready\n * rather than crashing the caller.\n *\n * SCOPE = every `required` fact resolves to a non-empty value.\n * SUBSTRATE = any value in `signals.substrate` is true.\n * READY = SCOPE && SUBSTRATE.\n *\n * `knownFacts` lists every fact (required or optional) that resolved to a\n * value, in spec declaration order; `missingFacts` lists every REQUIRED fact\n * that did not — optional facts never appear as missing because they never gate\n * scope.\n */\nexport function computeContextSufficiency(\n spec: ContextFactSpec,\n signals: ResolvedContextSignals,\n): ContextSufficiency {\n const facts = spec.facts ?? []\n const resolvedFacts = signals.facts ?? {}\n const substrate = signals.substrate ?? {}\n\n const knownFacts: KnownFact[] = []\n const missingFacts: MissingFact[] = []\n let hasScope = true\n\n for (const fact of facts) {\n const value = presentValue(resolvedFacts[fact.key])\n if (value !== undefined) {\n knownFacts.push({ key: fact.key, label: fact.label, value })\n } else if (fact.required) {\n hasScope = false\n missingFacts.push({ key: fact.key, label: fact.label, gatherHint: fact.gatherHint })\n }\n }\n\n const hasSubstrate = Object.values(substrate).some(Boolean)\n\n return {\n ready: hasScope && hasSubstrate,\n hasScope,\n hasSubstrate,\n knownFacts,\n missingFacts,\n substrate,\n }\n}\n\n/** The directive that turns gaps into conversational gathering, not a form. */\nconst GATHER_DIRECTIVE =\n 'Do not run an interview. Act on the message first, then fold AT MOST one or two pointed questions into the same turn to close the highest-leverage gap. Never present a form.'\n\n/** What to say when scope is met but no durable substrate exists yet. */\nconst SUBSTRATE_DIRECTIVE =\n 'You have the scope but no durable substrate to act on yet. As you work, persist what you learn — that is what makes this context-ready.'\n\n/**\n * Render the prompt section that mirrors a conversational-gather flow:\n * - \"### Context you already have\" — the known facts, as `label: value`.\n * - \"### Context still missing — gather it while you work, not as a form\" —\n * the missing facts (with their gather hints), the act-first directive, and\n * any product tool hints appended verbatim.\n *\n * When scope is met but the floor still is not (no substrate flag), it emits\n * the substrate directive instead of a missing-facts list. Returns '' when\n * there is nothing to say (no known facts, no gaps, and ready) so a caller can\n * concatenate it unconditionally. Pure: wording is product-neutral; every\n * product-specific phrase comes from the spec's labels, gather hints, and tool\n * hints.\n */\nexport function buildContextGatherPrompt(\n spec: ContextFactSpec,\n sufficiency: ContextSufficiency,\n): string {\n const lines: string[] = []\n\n if (sufficiency.knownFacts.length > 0) {\n lines.push('### Context you already have')\n lines.push(sufficiency.knownFacts.map((f) => `- ${f.label}: ${f.value}`).join('\\n'))\n }\n\n if (sufficiency.missingFacts.length > 0) {\n if (lines.length > 0) lines.push('')\n lines.push('### Context still missing — gather it while you work, not as a form')\n const gaps = sufficiency.missingFacts\n .map((f) => (f.gatherHint ? `- ${f.label} — ${f.gatherHint}` : `- ${f.label}`))\n .join('\\n')\n lines.push(`You do NOT have:\\n${gaps}`)\n lines.push(GATHER_DIRECTIVE)\n for (const hint of spec.toolHints ?? []) {\n const trimmed = hint.trim()\n if (trimmed) lines.push(trimmed)\n }\n } else if (!sufficiency.ready) {\n if (lines.length > 0) lines.push('')\n lines.push(SUBSTRATE_DIRECTIVE)\n for (const hint of spec.toolHints ?? []) {\n const trimmed = hint.trim()\n if (trimmed) lines.push(trimmed)\n }\n }\n\n if (lines.length === 0) return ''\n return `## Project Context & Sufficiency\\n${lines.join('\\n')}`\n}\n"],"mappings":"
|
|
1
|
+
{"version":3,"sources":["../../src/intakes/context-sufficiency.ts"],"sourcesContent":["/**\n * Pure context-sufficiency floor + conversational-gather scaffold — the\n * product-agnostic core of a CONVERSATIONAL intake. Where the question-graph\n * model (`./model`) runs a one-question-at-a-time interview, this leaf judges\n * whether an agent has gathered ENOUGH context to act, and renders the prompt\n * directive that tells the agent to fold the remaining gaps into its work\n * instead of running a form.\n *\n * Zero dependencies: no drizzle, no env, no react, no I/O, no product reads.\n * A product declares WHICH facts matter (`ContextFactSpec`), and its own\n * adapter resolves those facts + named substrate flags from whatever substrate\n * it owns (`ResolvedContextSignals`); this leaf only does the deterministic\n * combine and the wording. That separation is what makes the framework opt-in\n * and tree-shakeable, and what lets gtm/tax/legal/insurance share one core.\n *\n * Readiness is a two-part floor: SCOPE (every required fact has a value) AND\n * SUBSTRATE (at least one named substrate flag is true). Scope alone is not\n * enough — knowing the goal without any durable thing to act on is still\n * not-ready; substrate alone is not enough either. The product's adapter\n * decides what counts as a fact and what counts as substrate.\n */\n\n/** One fact the product treats as known context once it has a value. */\nexport interface ContextFact {\n /** Stable key the resolved-signals map is keyed on. */\n key: string\n /** Human label shown in the prompt's known/missing lists. */\n label: string\n /** When true, this fact must have a value for SCOPE to be met. */\n required?: boolean\n /** How the agent should gather this fact conversationally, if missing. */\n gatherHint?: string\n}\n\n/**\n * The product's declaration of what context matters: the facts that make up\n * scope, plus optional tool hints appended verbatim to the gather prompt (e.g.\n * a product passes the command that extracts a brand from a URL). Pure data —\n * the product supplies labels, hints, and tool lines; the framework never\n * names a product-specific concept itself.\n */\nexport interface ContextFactSpec {\n facts: ContextFact[]\n /** Lines appended after the gather directive — e.g. product tool pointers. */\n toolHints?: string[]\n}\n\n/**\n * What a product's adapter resolves from its own substrate, ready to combine.\n * `facts` carries the resolved VALUE per fact key (undefined/empty = not\n * known); `substrate` carries named boolean flags (e.g.\n * `{ brandConfirmed, configHasContext, coreKnowledgePresent }`) — any one true\n * satisfies the substrate half of the floor.\n */\nexport interface ResolvedContextSignals {\n facts: Record<string, string | undefined>\n substrate: Record<string, boolean>\n}\n\n/** A resolved fact that has a value — surfaced to the prompt as known context. */\nexport interface KnownFact {\n key: string\n label: string\n value: string\n}\n\n/** A required fact with no value — surfaced to the prompt as a gap to close. */\nexport interface MissingFact {\n key: string\n label: string\n gatherHint?: string\n}\n\n/** The deterministic verdict over the resolved signals. */\nexport interface ContextSufficiency {\n /** True when the floor is met: `hasScope && hasSubstrate`. */\n ready: boolean\n /** Every REQUIRED fact has a non-empty value. */\n hasScope: boolean\n /** At least one named substrate flag is true. */\n hasSubstrate: boolean\n /** Facts (required or not) that have a value. */\n knownFacts: KnownFact[]\n /** Required facts that have no value yet. */\n missingFacts: MissingFact[]\n /** The substrate flags as resolved, passed through for the prompt/caller. */\n substrate: Record<string, boolean>\n}\n\n/** Trim a fact value to a present string, or undefined when blank/absent. */\nfunction presentValue(value: string | undefined): string | undefined {\n if (typeof value !== 'string') return undefined\n const trimmed = value.trim()\n return trimmed.length > 0 ? trimmed : undefined\n}\n\n/**\n * Combine a fact spec with resolved signals into the readiness verdict. Pure,\n * deterministic, never throws — a missing fact key or an empty substrate map\n * reads as not-ready, not an error, so a fresh scope is honestly not-ready\n * rather than crashing the caller.\n *\n * SCOPE = every `required` fact resolves to a non-empty value.\n * SUBSTRATE = any value in `signals.substrate` is true.\n * READY = SCOPE && SUBSTRATE.\n *\n * `knownFacts` lists every fact (required or optional) that resolved to a\n * value, in spec declaration order; `missingFacts` lists every REQUIRED fact\n * that did not — optional facts never appear as missing because they never gate\n * scope.\n */\nexport function computeContextSufficiency(\n spec: ContextFactSpec,\n signals: ResolvedContextSignals,\n): ContextSufficiency {\n const facts = spec.facts ?? []\n const resolvedFacts = signals.facts ?? {}\n const substrate = signals.substrate ?? {}\n\n const knownFacts: KnownFact[] = []\n const missingFacts: MissingFact[] = []\n let hasScope = true\n\n for (const fact of facts) {\n const value = presentValue(resolvedFacts[fact.key])\n if (value !== undefined) {\n knownFacts.push({ key: fact.key, label: fact.label, value })\n } else if (fact.required) {\n hasScope = false\n missingFacts.push({ key: fact.key, label: fact.label, gatherHint: fact.gatherHint })\n }\n }\n\n const hasSubstrate = Object.values(substrate).some(Boolean)\n\n return {\n ready: hasScope && hasSubstrate,\n hasScope,\n hasSubstrate,\n knownFacts,\n missingFacts,\n substrate,\n }\n}\n\n/** The directive that turns gaps into conversational gathering, not a form. */\nconst GATHER_DIRECTIVE =\n 'Do not run an interview. Act on the message first, then fold AT MOST one or two pointed questions into the same turn to close the highest-leverage gap. Never present a form.'\n\n/** What to say when scope is met but no durable substrate exists yet. */\nconst SUBSTRATE_DIRECTIVE =\n 'You have the scope but no durable substrate to act on yet. As you work, persist what you learn — that is what makes this context-ready.'\n\n/**\n * Render the prompt section that mirrors a conversational-gather flow:\n * - \"### Context you already have\" — the known facts, as `label: value`.\n * - \"### Context still missing — gather it while you work, not as a form\" —\n * the missing facts (with their gather hints), the act-first directive, and\n * any product tool hints appended verbatim.\n *\n * When scope is met but the floor still is not (no substrate flag), it emits\n * the substrate directive instead of a missing-facts list. Returns '' when\n * there is nothing to say (no known facts, no gaps, and ready) so a caller can\n * concatenate it unconditionally. Pure: wording is product-neutral; every\n * product-specific phrase comes from the spec's labels, gather hints, and tool\n * hints.\n */\nexport function buildContextGatherPrompt(\n spec: ContextFactSpec,\n sufficiency: ContextSufficiency,\n): string {\n const lines: string[] = []\n\n if (sufficiency.knownFacts.length > 0) {\n lines.push('### Context you already have')\n lines.push(sufficiency.knownFacts.map((f) => `- ${f.label}: ${f.value}`).join('\\n'))\n }\n\n if (sufficiency.missingFacts.length > 0) {\n if (lines.length > 0) lines.push('')\n lines.push('### Context still missing — gather it while you work, not as a form')\n const gaps = sufficiency.missingFacts\n .map((f) => (f.gatherHint ? `- ${f.label} — ${f.gatherHint}` : `- ${f.label}`))\n .join('\\n')\n lines.push(`You do NOT have:\\n${gaps}`)\n lines.push(GATHER_DIRECTIVE)\n for (const hint of spec.toolHints ?? []) {\n const trimmed = hint.trim()\n if (trimmed) lines.push(trimmed)\n }\n } else if (!sufficiency.ready) {\n if (lines.length > 0) lines.push('')\n lines.push(SUBSTRATE_DIRECTIVE)\n for (const hint of spec.toolHints ?? []) {\n const trimmed = hint.trim()\n if (trimmed) lines.push(trimmed)\n }\n }\n\n if (lines.length === 0) return ''\n return `## Project Context & Sufficiency\\n${lines.join('\\n')}`\n}\n"],"mappings":";;;;;;;;;;;;;;;;AA0FA,SAAS,aAAa,OAA+C;AACnE,MAAI,OAAO,UAAU,SAAU,QAAO;AACtC,QAAM,UAAU,MAAM,KAAK;AAC3B,SAAO,QAAQ,SAAS,IAAI,UAAU;AACxC;AAiBO,SAAS,0BACd,MACA,SACoB;AACpB,QAAM,QAAQ,KAAK,SAAS,CAAC;AAC7B,QAAM,gBAAgB,QAAQ,SAAS,CAAC;AACxC,QAAM,YAAY,QAAQ,aAAa,CAAC;AAExC,QAAM,aAA0B,CAAC;AACjC,QAAM,eAA8B,CAAC;AACrC,MAAI,WAAW;AAEf,aAAW,QAAQ,OAAO;AACxB,UAAM,QAAQ,aAAa,cAAc,KAAK,GAAG,CAAC;AAClD,QAAI,UAAU,QAAW;AACvB,iBAAW,KAAK,EAAE,KAAK,KAAK,KAAK,OAAO,KAAK,OAAO,MAAM,CAAC;AAAA,IAC7D,WAAW,KAAK,UAAU;AACxB,iBAAW;AACX,mBAAa,KAAK,EAAE,KAAK,KAAK,KAAK,OAAO,KAAK,OAAO,YAAY,KAAK,WAAW,CAAC;AAAA,IACrF;AAAA,EACF;AAEA,QAAM,eAAe,OAAO,OAAO,SAAS,EAAE,KAAK,OAAO;AAE1D,SAAO;AAAA,IACL,OAAO,YAAY;AAAA,IACnB;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,EACF;AACF;AAGA,IAAM,mBACJ;AAGF,IAAM,sBACJ;AAgBK,SAAS,yBACd,MACA,aACQ;AACR,QAAM,QAAkB,CAAC;AAEzB,MAAI,YAAY,WAAW,SAAS,GAAG;AACrC,UAAM,KAAK,8BAA8B;AACzC,UAAM,KAAK,YAAY,WAAW,IAAI,CAAC,MAAM,KAAK,EAAE,KAAK,KAAK,EAAE,KAAK,EAAE,EAAE,KAAK,IAAI,CAAC;AAAA,EACrF;AAEA,MAAI,YAAY,aAAa,SAAS,GAAG;AACvC,QAAI,MAAM,SAAS,EAAG,OAAM,KAAK,EAAE;AACnC,UAAM,KAAK,0EAAqE;AAChF,UAAM,OAAO,YAAY,aACtB,IAAI,CAAC,MAAO,EAAE,aAAa,KAAK,EAAE,KAAK,WAAM,EAAE,UAAU,KAAK,KAAK,EAAE,KAAK,EAAG,EAC7E,KAAK,IAAI;AACZ,UAAM,KAAK;AAAA,EAAqB,IAAI,EAAE;AACtC,UAAM,KAAK,gBAAgB;AAC3B,eAAW,QAAQ,KAAK,aAAa,CAAC,GAAG;AACvC,YAAM,UAAU,KAAK,KAAK;AAC1B,UAAI,QAAS,OAAM,KAAK,OAAO;AAAA,IACjC;AAAA,EACF,WAAW,CAAC,YAAY,OAAO;AAC7B,QAAI,MAAM,SAAS,EAAG,OAAM,KAAK,EAAE;AACnC,UAAM,KAAK,mBAAmB;AAC9B,eAAW,QAAQ,KAAK,aAAa,CAAC,GAAG;AACvC,YAAM,UAAU,KAAK,KAAK;AAC1B,UAAI,QAAS,OAAM,KAAK,OAAO;AAAA,IACjC;AAAA,EACF;AAEA,MAAI,MAAM,WAAW,EAAG,QAAO;AAC/B,SAAO;AAAA,EAAqC,MAAM,KAAK,IAAI,CAAC;AAC9D;","names":[]}
|