@nuxtagentic/prompt 0.0.0-stage → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (36) hide show
  1. package/LICENSE.md +9 -0
  2. package/README.md +91 -2
  3. package/cloudconfig.ts +165 -0
  4. package/config.ts +33 -0
  5. package/core/decisions.ts +155 -0
  6. package/core/engine.ts +317 -0
  7. package/core/index.ts +3 -0
  8. package/core/planner.ts +67 -0
  9. package/core/types.ts +188 -0
  10. package/core/validation.ts +187 -0
  11. package/docs/search.md +69 -0
  12. package/module.ts +26 -0
  13. package/package.json +56 -3
  14. package/runtime/server/api/agent/cancel.post.ts +17 -0
  15. package/runtime/server/api/agent/execute.post.ts +26 -0
  16. package/runtime/server/api/agent/plan.post.ts +37 -0
  17. package/runtime/server/api/agent.post.ts +60 -0
  18. package/runtime/server/confirmations.ts +104 -0
  19. package/runtime/server/utils/agent-operations.ts +112 -0
  20. package/runtime/server/utils/agent-session.ts +14 -0
  21. package/runtime/server/utils/decision-arguments.ts +363 -0
  22. package/runtime/server/utils/decision-provider.ts +141 -0
  23. package/runtime/server/utils/jev-answers.ts +55 -0
  24. package/runtime/server/utils/jev-dispatch.ts +156 -0
  25. package/runtime/server/utils/jev-inference.ts +87 -0
  26. package/runtime/server/utils/jev-tool-plan.ts +64 -0
  27. package/runtime/server/utils/model-query-condition.ts +59 -0
  28. package/runtime/server/utils/model-query.ts +275 -0
  29. package/runtime/server/utils/prompt-candidates.ts +165 -0
  30. package/runtime/server/utils/prompt-dates.ts +83 -0
  31. package/runtime/shared/agent-tools.ts +50 -0
  32. package/runtime/shared/agent.ts +5 -0
  33. package/runtime/shared/decision-provider.ts +1 -0
  34. package/runtime/shared/prompt-config.ts +27 -0
  35. package/schema.ts +69 -0
  36. package/server-contributions.ts +53 -0
package/LICENSE.md ADDED
@@ -0,0 +1,9 @@
1
+ Proprietary software. All rights reserved.
2
+
3
+ Use of Nuxt Agentic packages and starter materials is governed by the applicable
4
+ commercial agreement supplied with a purchase. This repository does not grant
5
+ an open-source license or permission to redistribute the packages as a competing
6
+ template or module library.
7
+
8
+ Third-party dependencies retain their respective licenses. This notice does not
9
+ replace or modify those licenses.
package/README.md CHANGED
@@ -1,3 +1,92 @@
1
- # Temporary Holding Version
1
+ # Prompt
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ `@nuxtagentic/prompt` owns decision providers, prompt dispatch, schema-based query
4
+ interpretation, and the plan/review/execute lifecycle. It installs Data and has no
5
+ dependency on a particular collection UI. The root app supplies the prompt and review
6
+ dialog; installed features supply their tools, model views, and resource adapters.
7
+
8
+ This shared prompt engine serves the workspace input. The separate
9
+ [`@nuxtagentic/agents`](../agents/README.md) package manages saved agent
10
+ definitions and their runs. The `/api/agent` endpoints and `agentOperation`
11
+ confirmation model retain their existing names.
12
+
13
+ The prompt uses registered feature aliases, tools, and model views. Agents,
14
+ Integrations, and Automations contribute their navigation and planning metadata.
15
+ New domain features can add structured parsing and natural-language actions
16
+ through the same contracts without a separate prompt engine.
17
+
18
+ ## Setup
19
+
20
+ Include `agentOperationsModels` from `@nuxtagentic/prompt/schema` in the host's
21
+ `cloud.config.ts`, then generate, review, and apply the CloudConfig migration.
22
+ The confirmation store must exist before planning mutations.
23
+
24
+ Set `NUXT_DECISION_PROVIDER` to `typesafeAI` (default) or `openAI`, and configure
25
+ `NUXT_TYPESAFE_API_KEY` or `NUXT_OPENAI_API_KEY` on the server.
26
+ `NUXT_OPENAI_DECISION_MODEL` overrides the default OpenAI decision model,
27
+ `gpt-6-luna`. Restart or redeploy after changing settings. Provider keys stay out
28
+ of public runtime config.
29
+
30
+ ## Follow a request
31
+
32
+ | Endpoint | Purpose |
33
+ | ------------------------- | ------------------------------------------------------------------- |
34
+ | `POST /api/agent` | Route a collection prompt and produce validated tool arguments |
35
+ | `POST /api/agent/plan` | Return results, clarification, upload handoff, or a mutation review |
36
+ | `POST /api/agent/execute` | Execute a review key and selected IDs; return a receipt |
37
+ | `POST /api/agent/cancel` | Cancel a pending review |
38
+
39
+ Both providers select typed choices for resources, operations, fields, operators,
40
+ and literal values. Choices come from registered schemas and the request text.
41
+ The application constructs and validates filters and writes; providers do not
42
+ supply arbitrary SQL or execute tools. Missing required fields produce a specific
43
+ clarification. Unsupported values and ambiguous timestamps remain reviewable errors.
44
+
45
+ Feature metadata enters through `#workspace/client` and `#workspace/server`.
46
+ A feature's `runtime/shared/agent.ts` contributes public views, tools, aliases,
47
+ and exclusions. Its `runtime/server/contribution.ts` can add authenticated
48
+ resource factories for its own data and actions. There is no host-level action or model-view configuration file.
49
+
50
+ Only models contributed through installed resource aliases or model views enter
51
+ the agent catalog. Keeping a removed feature's schema does not expose its records
52
+ or internal workflows. Custom models retain schema-derived fields and operations;
53
+ register their aliases or views to make them available.
54
+
55
+ Editor commands live in each feature's `runtime/actions.ts` and execute through
56
+ Workspace's registered handlers. They remain separate from database mutations.
57
+
58
+ ## Mutation guarantees
59
+
60
+ Every mutation opens a review. Updates and deletes require an ID, current
61
+ selection, or a nonempty filter matching at most 25 accessible records. LIMIT
62
+ cannot silently truncate mutation targets. Singular requests select one record;
63
+ plural requests can select a subset.
64
+
65
+ Reviews bind the proposed input, candidate IDs, and fingerprints to the account
66
+ and session and expire after ten minutes. Execution accepts the key and reviewed
67
+ IDs, not replacement fields or filters. It checks authorization and freshness,
68
+ claims the review atomically, and stores the receipt for retries.
69
+
70
+ Batches execute sequentially. A partial failure records completed IDs and prevents
71
+ replay. This is not a cross-model transaction; an uncertain execution stays blocked
72
+ until its outcome is known. CloudConfig still enforces model rules, validation,
73
+ hooks, and storage cleanup for each write.
74
+
75
+ ## Find the code
76
+
77
+ | Area | Owner |
78
+ | -------------------------------------------- | ---------------------------------------------------------------------------------------------- |
79
+ | Framework-independent planning and execution | [`core/`](core/) |
80
+ | CloudConfig resources | [`cloudconfig.ts`](cloudconfig.ts) |
81
+ | Provider selection and HTTP adapters | [`runtime/server/utils/decision-provider.ts`](runtime/server/utils/decision-provider.ts) |
82
+ | Collection routing | [`runtime/server/utils/jev-dispatch.ts`](runtime/server/utils/jev-dispatch.ts) |
83
+ | Mutation arguments | [`runtime/server/utils/decision-arguments.ts`](runtime/server/utils/decision-arguments.ts) |
84
+ | Model query compilation | [`runtime/server/utils/model-query.ts`](runtime/server/utils/model-query.ts) |
85
+ | Authenticated operation host | [`runtime/server/utils/agent-operations.ts`](runtime/server/utils/agent-operations.ts) |
86
+ | Durable reviews and receipts | [`runtime/server/confirmations.ts`](runtime/server/confirmations.ts), [`schema.ts`](schema.ts) |
87
+ | Query limits and inference policy | [`config.ts`](config.ts) |
88
+ | Review dialog | Host application: `app/components/AgentOperationDialog.vue` |
89
+
90
+ [How search works](docs/search.md) covers local parsing, collection registration,
91
+ and provider boundaries. Regression tests live in `tests/`; the host's
92
+ `pnpm test:workspace` includes them. Default verification remains `pnpm typecheck`.
package/cloudconfig.ts ADDED
@@ -0,0 +1,165 @@
1
+ import type { AnyConfig, CloudConfigUser, FilterNode } from '@cloudconfig/core'
2
+ import { compileConfig, publicModels, toInputSchema } from '@cloudconfig/core/server'
3
+ import {
4
+ OPERATIONS,
5
+ OperationError,
6
+ type Field,
7
+ type Query,
8
+ type Resource,
9
+ type Row,
10
+ } from './core/types'
11
+ import { isRecord } from './core/validation'
12
+
13
+ /** Type erasure is confined to the registry boundary; hosts instantiate the generated typed clients. */
14
+ export interface ModelSource {
15
+ list(query?: Query): Promise<{ data: Row[]; totalCount: number }>
16
+ get(id: number): Promise<Row>
17
+ create(input: Record<string, unknown>): Promise<Row>
18
+ update(id: number, input: Record<string, unknown>): Promise<Row>
19
+ delete(id: number): Promise<void>
20
+ }
21
+ interface AdapterOptions {
22
+ user: CloudConfigUser
23
+ request: Request
24
+ source: (name: string) => ModelSource
25
+ /** Used only for permission preflight predicates that refer to private ownership fields. */
26
+ privateSource: (name: string) => Pick<ModelSource, 'list'>
27
+ include?: readonly string[]
28
+ exclude?: readonly string[]
29
+ aliases?: Record<string, readonly string[]>
30
+ lockedFields?: readonly string[]
31
+ }
32
+
33
+ /** Derive capabilities and public write schemas from the SAME config as the generated REST API. */
34
+ export function createCloudConfigResources(config: AnyConfig, options: AdapterOptions): Resource[] {
35
+ const base = compileConfig(config)
36
+ const locked = new Set(options.lockedFields ?? ['ownerId', 'editorIds', 'viewerIds'])
37
+ return publicModels(base)
38
+ .filter(
39
+ (model) =>
40
+ (!options.include || options.include.includes(model.name)) &&
41
+ !options.exclude?.includes(model.name),
42
+ )
43
+ .map((model) => {
44
+ const source = options.source(model.name)
45
+ const fields: Field[] = [
46
+ { key: 'id', label: 'ID', kind: 'number', required: false, writable: false },
47
+ ...Object.values(model.fieldSpecs)
48
+ .filter(({ spec }) => !spec.serverOnly)
49
+ .map(({ columnKey, spec }) => ({
50
+ key: columnKey,
51
+ label: spec.label ?? columnKey,
52
+ kind: spec.kind,
53
+ required: spec.required && !spec.hasDefault && !locked.has(columnKey),
54
+ options: spec.options,
55
+ writable: !locked.has(columnKey),
56
+ })),
57
+ ...(model.timestamps
58
+ ? ['createdAt', 'updatedAt'].map((key) => ({
59
+ key,
60
+ label: key,
61
+ kind: 'datetime',
62
+ required: false,
63
+ writable: false,
64
+ }))
65
+ : []),
66
+ ]
67
+ const schemaKeys = (schema: typeof model.create) =>
68
+ Object.keys((toInputSchema(schema).properties ?? {}) as Record<string, unknown>).filter(
69
+ (key) => !locked.has(key),
70
+ )
71
+ const createFields = schemaKeys(model.create)
72
+ const updateFields = schemaKeys(model.update)
73
+ for (const field of fields) {
74
+ field.writable = createFields.includes(field.key) || updateFields.includes(field.key)
75
+ }
76
+ const operations = OPERATIONS.filter(
77
+ (action) => model.model.options.access?.[action] || base.accessDefault === 'allow',
78
+ )
79
+ const resource: Resource = {
80
+ id: model.name,
81
+ label: model.label,
82
+ aliases: [model.name, model.namePlural, ...(options.aliases?.[model.name] ?? [])],
83
+ fields,
84
+ operations,
85
+ createFields,
86
+ updateFields,
87
+ displayFields: fields
88
+ .filter(
89
+ (field) =>
90
+ !['json', 'multiselect', 'relation'].includes(field.kind) && !locked.has(field.key),
91
+ )
92
+ .map((field) => field.key),
93
+ ...(model.model.options.storage ? { createHandoff: `upload:${model.name}` } : {}),
94
+ list: (query) => source.list(query),
95
+ get: (id) => source.get(id),
96
+ create: (input) => source.create(input),
97
+ update: (id, input) => source.update(id, input),
98
+ delete: (id) => source.delete(id),
99
+ validateInput(action, input) {
100
+ const allowed = action === 'create' ? createFields : updateFields
101
+ if (!isRecord(input) || Object.keys(input).some((key) => !allowed.includes(key))) {
102
+ throw new OperationError(
103
+ 422,
104
+ 'The action contains a server-managed or unavailable field.',
105
+ )
106
+ }
107
+ // A public ownership column is assigned from the authenticated session, never from AI input.
108
+ const supplied = { ...input }
109
+ if (
110
+ action === 'create' &&
111
+ model.fieldSpecs.ownerId &&
112
+ !model.fieldSpecs.ownerId.spec.serverOnly
113
+ ) {
114
+ supplied.ownerId = options.user.id
115
+ }
116
+ const parsed = (action === 'create' ? model.create : model.update).safeParse(supplied)
117
+ if (!parsed.success) {
118
+ const first = parsed.error.issues[0]
119
+ throw new OperationError(
120
+ 422,
121
+ first ? `${first.path.join('.')}: ${first.message}` : 'Invalid record fields.',
122
+ )
123
+ }
124
+ if (!isRecord(parsed.data)) {
125
+ throw new OperationError(422, 'Invalid record fields.')
126
+ }
127
+ return parsed.data
128
+ },
129
+ async authorize(action, id, input) {
130
+ const rule = model.model.options.access?.[action]
131
+ const result = rule
132
+ ? await rule({
133
+ user: options.user,
134
+ request: options.request,
135
+ action,
136
+ ...(id !== undefined ? { id } : {}),
137
+ ...(input !== undefined ? { input } : {}),
138
+ where: (field, operator, value) => ({ type: 'condition', field, operator, value }),
139
+ })
140
+ : base.accessDefault === 'allow'
141
+ if (result === false) {
142
+ throw new OperationError(403, `You cannot ${action} ${model.label.toLowerCase()}.`)
143
+ }
144
+ if (typeof result !== 'boolean' && id !== undefined) {
145
+ const filters = {
146
+ type: 'group' as const,
147
+ logic: 'and' as const,
148
+ children: [
149
+ { type: 'condition' as const, field: 'id', operator: 'eq' as const, value: id },
150
+ result as FilterNode,
151
+ ],
152
+ }
153
+ // CloudConfig evaluates the list rule and this write predicate together. Private rows stay here.
154
+ const found = await options
155
+ .privateSource(model.name)
156
+ .list({ page: 1, pageSize: 1, filters })
157
+ if (!found.data.length) {
158
+ throw new OperationError(403, `You cannot ${action} this record.`)
159
+ }
160
+ }
161
+ },
162
+ }
163
+ return resource
164
+ })
165
+ }
package/config.ts ADDED
@@ -0,0 +1,33 @@
1
+ import type { PromptConfig } from './runtime/shared/prompt-config'
2
+
3
+ export const MODEL_QUERY_CONFIG = {
4
+ model: 'jev-latest',
5
+ maxConstraintsPerField: 4,
6
+ candidates: { span: 24, text: 200, maxWords: 8 },
7
+ routing: { maxInlineModels: 2, maxInlineQuestions: 120, maxInlineBytes: 64000 },
8
+ inference: { deadlineMs: 10000, timeoutMs: 5000, maxRetries: 1 },
9
+ rerank: {
10
+ maxFiles: 12,
11
+ excerptChars: 500,
12
+ queryChars: 300,
13
+ nameChars: 255,
14
+ folderChars: 80,
15
+ mimeTypeChars: 100,
16
+ deadlineMs: 5000,
17
+ timeoutMs: 3000,
18
+ maxRetries: 0,
19
+ },
20
+ logMetrics: false,
21
+ instructions: [
22
+ 'Interpret the intended read-only query, whether written as SQL, shorthand, or natural language.',
23
+ 'Accept omitted WHERE, missing operators, singular/plural names, reordered words, synonyms, and obvious spelling mistakes when the allowed schema makes the intent clear.',
24
+ 'For boolean fields, true/yes/on/1 mean true and false/no/off/0 mean false. Respect negation and the meaning of the field.',
25
+ 'A boolean adjective alone is a filter. A field followed by a value implies equality unless a different comparison is requested.',
26
+ 'Use field keys, labels, and optional field hints to match concepts. Never invent a field, value, or constraint, and never discard an unsupported part of the request.',
27
+ 'Treat text values as data: preserve their spelling, punctuation, and full wording. Correct spelling only when matching schema vocabulary.',
28
+ 'Expand IN/one-of alternatives into equality constraints joined with OR; NOT IN/excluding alternatives into inequality constraints joined with AND. A range has two comparisons.',
29
+ 'Number distinct constraints on each field by their appearance in the request. A named date period is one constraint. Do not repeat a constraint in another slot.',
30
+ 'A range uses two constraints, each IN/NOT IN value uses one. Generic search is separate from explicit field filters. Not mentioned and mentioned with an unavailable value are different outcomes.',
31
+ 'Month names without a year use the current UTC year from referenceDate. Compare named date periods as a whole: = means during, != means outside, > means after the period ends, >= means from its start, < means before its start, and <= means through its end.',
32
+ ].join(' '),
33
+ } satisfies PromptConfig
@@ -0,0 +1,155 @@
1
+ import type { EntryType, Questions, SystemOneRequest, SystemOneResult } from '@typesafe-ai/sdk'
2
+ import { OperationError } from './types'
3
+ import { isRecord } from './validation'
4
+
5
+ export type DecisionResult = SystemOneResult<Questions>
6
+ export type DecisionInference = (request: SystemOneRequest) => Promise<DecisionResult>
7
+ export type DecisionInput = {
8
+ role: 'user'
9
+ content: ({ type: 'input_text'; text: string } | { type: 'input_image'; image_url: string })[]
10
+ }[]
11
+
12
+ const text = (value: EntryType | undefined) =>
13
+ typeof value === 'string' ? value : JSON.stringify(value ?? '')
14
+ const probability = (value: unknown): value is number =>
15
+ typeof value === 'number' && Number.isFinite(value) && value >= 0 && value <= 1
16
+
17
+ export function openAIDecisionBody(
18
+ request: SystemOneRequest,
19
+ model: string,
20
+ input?: DecisionInput,
21
+ ) {
22
+ return {
23
+ model,
24
+ input: input ?? text(request.state),
25
+ questions: Object.entries(request.questions).map(([name, question]) => {
26
+ const base = { name, instructions: text(question.instructions) }
27
+ switch (question.type) {
28
+ case 'choice':
29
+ return {
30
+ ...base,
31
+ type: 'choice',
32
+ choices: Object.entries(question.criteria).map(([value, description]) => ({
33
+ value,
34
+ description: text(description),
35
+ })),
36
+ }
37
+ case 'noul':
38
+ return {
39
+ ...base,
40
+ type: 'predicate',
41
+ instructions: question.criteria
42
+ ? `${base.instructions} Outcome criteria: ${text(question.criteria)}`
43
+ : base.instructions,
44
+ }
45
+ case 'score':
46
+ return {
47
+ ...base,
48
+ type: 'score',
49
+ levels: question.criteria.map((description, index) => ({
50
+ label: String(index),
51
+ description: text(description),
52
+ })),
53
+ }
54
+ }
55
+ }),
56
+ }
57
+ }
58
+
59
+ export function readOpenAIDecisions(
60
+ value: unknown,
61
+ request: SystemOneRequest,
62
+ model: string,
63
+ ): DecisionResult {
64
+ if (!isRecord(value) || !Array.isArray(value.answers)) {
65
+ throw new OperationError(502, 'OpenAI returned an invalid decision.')
66
+ }
67
+ const answers: Record<string, DecisionResult['answers'][string]> = {}
68
+ for (const answer of value.answers) {
69
+ if (
70
+ !isRecord(answer) ||
71
+ typeof answer.name !== 'string' ||
72
+ Object.hasOwn(answers, answer.name)
73
+ ) {
74
+ throw new OperationError(502, 'OpenAI returned an invalid decision.')
75
+ }
76
+ const {
77
+ name,
78
+ type,
79
+ probability: answerProbability,
80
+ confidence,
81
+ probabilities: answerProbabilities,
82
+ choice,
83
+ score,
84
+ } = answer
85
+ if (type === 'refusal') {
86
+ throw new OperationError(
87
+ 422,
88
+ 'The request could not be interpreted. Rephrase the action and resource.',
89
+ )
90
+ }
91
+ const question = request.questions[name]
92
+ if (!question) {
93
+ throw new OperationError(502, 'OpenAI returned an unavailable decision.')
94
+ }
95
+ if (type === 'predicate' && question.type === 'noul') {
96
+ if (!probability(answerProbability)) {
97
+ throw new OperationError(502, 'OpenAI returned an invalid probability.')
98
+ }
99
+ answers[name] = { type: 'noul', noul: answerProbability }
100
+ continue
101
+ }
102
+ if (!probability(confidence)) {
103
+ throw new OperationError(502, 'OpenAI returned an invalid confidence.')
104
+ }
105
+ const probabilities = Object.fromEntries(
106
+ (Array.isArray(answerProbabilities) ? answerProbabilities : []).map((item) => {
107
+ if (!isRecord(item) || !probability(item.probability)) {
108
+ throw new OperationError(502, 'OpenAI returned invalid probabilities.')
109
+ }
110
+ return [String(item.value), item.probability]
111
+ }),
112
+ )
113
+ if (type === 'choice' && question.type === 'choice') {
114
+ if (typeof choice !== 'string' || !Object.hasOwn(question.criteria, choice)) {
115
+ throw new OperationError(502, 'OpenAI returned an unavailable choice.')
116
+ }
117
+ answers[name] = {
118
+ type: 'choice',
119
+ choice,
120
+ confidence,
121
+ probabilities,
122
+ }
123
+ } else if (type === 'score' && question.type === 'score') {
124
+ if (
125
+ typeof score !== 'number' ||
126
+ !Number.isFinite(score) ||
127
+ score < 0 ||
128
+ score > question.criteria.length - 1
129
+ ) {
130
+ throw new OperationError(502, 'OpenAI returned an invalid score.')
131
+ }
132
+ answers[name] = {
133
+ type: 'score',
134
+ score,
135
+ confidence,
136
+ probabilities,
137
+ legend: Object.fromEntries(question.criteria.map((level, index) => [index, level])),
138
+ }
139
+ } else {
140
+ throw new OperationError(502, 'OpenAI returned an invalid decision type.')
141
+ }
142
+ }
143
+ if (Object.keys(request.questions).some((name) => !Object.hasOwn(answers, name))) {
144
+ throw new OperationError(502, 'OpenAI omitted a decision.')
145
+ }
146
+ const usage = isRecord(value.usage) ? value.usage : {}
147
+ return {
148
+ model: typeof value.model === 'string' ? value.model : model,
149
+ answers,
150
+ usage: {
151
+ input_tokens: Number(usage.input_tokens ?? 0),
152
+ output_tokens: Number(usage.output_tokens ?? 0),
153
+ },
154
+ }
155
+ }