@nuxtagentic/prompt 0.0.0-stage → 0.1.1

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
@@ -0,0 +1,165 @@
1
+ import type { Scalar } from '@nuxtagentic/data/runtime/shared/resource-query'
2
+ import type { PromptConfig } from '../../shared/prompt-config'
3
+ import { findDateCandidates } from './prompt-dates'
4
+
5
+ export interface ValueCandidate {
6
+ value: Scalar
7
+ label: string
8
+ end?: string
9
+ }
10
+
11
+ interface NumberCandidate extends ValueCandidate {
12
+ value: number
13
+ unit?: string
14
+ }
15
+
16
+ export interface PromptCandidates {
17
+ now: Date
18
+ text: string[]
19
+ spans: string[]
20
+ numbers: NumberCandidate[]
21
+ dates: ValueCandidate[]
22
+ limits: string[]
23
+ }
24
+
25
+ // Rank phrase boundaries; never remove these words from a literal value.
26
+ const STOPWORDS = new Set(
27
+ 'a an the and or of in on at to for from by with without is are was were be been has have had do does did me my we our us you your i it its all any every some only just please can could would show list find get give see display filter search sort order named called titled where what when how'.split(
28
+ ' ',
29
+ ),
30
+ )
31
+
32
+ function textCandidates(prompt: string, maxWords: number, limit: number): string[] {
33
+ const quoted = [...prompt.matchAll(/"([^"]+)"|(?<![\p{L}\p{N}])'([^']+)'|“([^”]+)”/gu)].map(
34
+ (match) => (match[1] ?? match[2] ?? match[3])!.trim(),
35
+ )
36
+ const emails = prompt.match(/[\w.+-]+@[\w-]+(?:\.[\w-]+)+/g) ?? []
37
+ const named = [
38
+ ...prompt.matchAll(
39
+ /\b(?:named|called|titled)\s+(.+?)(?=\s+(?:on|at|from|for|where|with)\b|[.!?]?$)/gi,
40
+ ),
41
+ ].map((match) => match[1]!.trim())
42
+ const words = [...prompt.matchAll(/[\p{L}\p{N}@._+-]+(?:['’][\p{L}\p{N}]+)*/gu)]
43
+ const groups = Array.from({ length: Math.min(maxWords, words.length) }, (_, index) => {
44
+ const length = index + 1
45
+ const spans = []
46
+ for (let i = 0; i + length <= words.length; i++) {
47
+ const first = words[i]!
48
+ const last = words[i + length - 1]!
49
+ spans.push({
50
+ text: prompt.slice(first.index!, last.index! + last[0].length),
51
+ rank:
52
+ Number(!STOPWORDS.has(first[0].toLowerCase())) +
53
+ Number(!STOPWORDS.has(last[0].toLowerCase())),
54
+ })
55
+ }
56
+ return spans.sort((a, b) => b.rank - a.rank)
57
+ })
58
+ const result = new Set([...quoted, ...emails, ...named].filter(Boolean))
59
+ for (let offset = 0; result.size < limit; offset++) {
60
+ let found = false
61
+ for (const group of groups) {
62
+ const span = group[offset]
63
+ if (!span) {
64
+ continue
65
+ }
66
+ found = true
67
+ result.add(span.text)
68
+ if (result.size >= limit) {
69
+ break
70
+ }
71
+ }
72
+ if (!found) {
73
+ break
74
+ }
75
+ }
76
+ return [...result].slice(0, limit)
77
+ }
78
+
79
+ export function prepareCandidates(
80
+ prompt: string,
81
+ config: PromptConfig,
82
+ now = new Date(),
83
+ ): PromptCandidates {
84
+ const text = textCandidates(
85
+ prompt,
86
+ config.candidates.maxWords,
87
+ Math.min(253, Math.max(config.candidates.text, config.candidates.span)),
88
+ )
89
+ const numbers = [...prompt.matchAll(/-?\d+(?:\.\d+)?(?:\s*(?:KB|MB|GB))?/gi)].map((match) => {
90
+ const [, amount, unit] = /(-?\d+(?:\.\d+)?)\s*(KB|MB|GB)?/i.exec(match[0])!
91
+ return { value: Number(amount), label: match[0], unit: unit?.toUpperCase() }
92
+ })
93
+ return {
94
+ now,
95
+ text: text.slice(0, config.candidates.text),
96
+ spans: text.slice(0, config.candidates.span),
97
+ numbers,
98
+ dates: findDateCandidates(prompt, now),
99
+ limits: [...new Set(prompt.match(/\b\d+\b/g) ?? [])].slice(0, 253),
100
+ }
101
+ }
102
+
103
+ export function findFieldCandidates(
104
+ context: PromptCandidates,
105
+ field: { kind: string; options?: readonly string[] },
106
+ vocabulary: Record<string, string> = {},
107
+ unit?: 'bytes',
108
+ ): ValueCandidate[] {
109
+ let values: ValueCandidate[]
110
+ switch (field.kind) {
111
+ case 'boolean':
112
+ values = [
113
+ { value: true, label: 'true / yes / on / 1' },
114
+ { value: false, label: 'false / no / off / 0' },
115
+ ]
116
+ break
117
+ case 'select':
118
+ values = (field.options ?? []).map((value) => ({
119
+ value,
120
+ label: vocabulary[value] ? `${value}: ${vocabulary[value]}` : value,
121
+ }))
122
+ break
123
+ case 'number':
124
+ case 'float':
125
+ case 'relation':
126
+ values = context.numbers.map((candidate) => ({
127
+ value:
128
+ candidate.value *
129
+ (unit === 'bytes' && candidate.unit
130
+ ? { KB: 1024, MB: 1024 ** 2, GB: 1024 ** 3 }[candidate.unit as 'KB' | 'MB' | 'GB']
131
+ : 1),
132
+ label: candidate.label,
133
+ }))
134
+ break
135
+ case 'date':
136
+ case 'datetime':
137
+ values = context.dates.map((candidate) =>
138
+ field.kind === 'date'
139
+ ? {
140
+ ...candidate,
141
+ value: String(candidate.value).slice(0, 10),
142
+ end: candidate.end?.slice(0, 10),
143
+ }
144
+ : candidate,
145
+ )
146
+ break
147
+ default:
148
+ values = [
149
+ ...Object.entries(vocabulary).map(([value, meaning]) => ({
150
+ value,
151
+ label: `${value}: ${meaning}`,
152
+ })),
153
+ ...context.text.map((value) => ({ value, label: value })),
154
+ ]
155
+ }
156
+ const unique = new Map<string, ValueCandidate>()
157
+ for (const candidate of values) {
158
+ const key = JSON.stringify([candidate.value, candidate.end])
159
+ if (!unique.has(key)) {
160
+ unique.set(key, candidate)
161
+ }
162
+ }
163
+ // Reserve three of Choice's 255 options: actual null, absent and unavailable.
164
+ return [...[...unique.values()].slice(0, 252), { value: null, label: 'missing / null' }]
165
+ }
@@ -0,0 +1,83 @@
1
+ import { findNamedMonthPeriods } from '@nuxtagentic/data/runtime/shared/date-periods'
2
+ import type { ValueCandidate } from './prompt-candidates'
3
+
4
+ const millisecondsPerDay = 86_400_000
5
+ const millisecondsPerUnit: Record<string, number> = {
6
+ hour: 3_600_000,
7
+ day: millisecondsPerDay,
8
+ week: 7 * millisecondsPerDay,
9
+ }
10
+
11
+ export function findDateCandidates(prompt: string, now: Date): ValueCandidate[] {
12
+ const values: ValueCandidate[] = []
13
+ const day = (year: number, month: number, date: number) =>
14
+ new Date(Date.UTC(year, month, date)).toISOString()
15
+ const year = now.getUTCFullYear()
16
+ const month = now.getUTCMonth()
17
+ const date = now.getUTCDate()
18
+ const weekday = (now.getUTCDay() + 6) % 7
19
+ const periods: Record<string, [string, string]> = {
20
+ today: [day(year, month, date), day(year, month, date + 1)],
21
+ yesterday: [day(year, month, date - 1), day(year, month, date)],
22
+ tomorrow: [day(year, month, date + 1), day(year, month, date + 2)],
23
+ 'this week': [day(year, month, date - weekday), day(year, month, date - weekday + 7)],
24
+ 'last week': [day(year, month, date - weekday - 7), day(year, month, date - weekday)],
25
+ 'next week': [day(year, month, date - weekday + 7), day(year, month, date - weekday + 14)],
26
+ 'this month': [day(year, month, 1), day(year, month + 1, 1)],
27
+ 'last month': [day(year, month - 1, 1), day(year, month, 1)],
28
+ 'next month': [day(year, month + 1, 1), day(year, month + 2, 1)],
29
+ 'this year': [day(year, 0, 1), day(year + 1, 0, 1)],
30
+ 'last year': [day(year - 1, 0, 1), day(year, 0, 1)],
31
+ 'next year': [day(year + 1, 0, 1), day(year + 2, 0, 1)],
32
+ }
33
+ for (const [label, [value, end]] of Object.entries(periods)) {
34
+ if (new RegExp(`\\b${label}\\b`, 'i').test(prompt)) {
35
+ values.push({ label: `${label} (${value} until ${end}, exclusive)`, value, end })
36
+ }
37
+ }
38
+ for (const { label, start, end } of findNamedMonthPeriods(prompt, now)) {
39
+ values.push({ label: `${label} (${start} until ${end}, exclusive)`, value: start, end })
40
+ }
41
+ for (const match of prompt.matchAll(
42
+ /\b(?:last|past|previous)\s+(\d+)\s+(hours?|days?|weeks?|months?|years?)\b/gi,
43
+ )) {
44
+ const amount = Number(match[1])
45
+ const unit = match[2]!.toLowerCase().replace(/s$/, '')
46
+ const start = new Date(now)
47
+ if (unit === 'year' || unit === 'month') {
48
+ const targetYear = unit === 'year' ? year - amount : year
49
+ const targetMonth = unit === 'month' ? month - amount : month
50
+ const lastDay = new Date(Date.UTC(targetYear, targetMonth + 1, 0)).getUTCDate()
51
+ start.setUTCDate(Math.min(date, lastDay))
52
+ start.setUTCFullYear(targetYear, targetMonth)
53
+ } else {
54
+ const duration = millisecondsPerUnit[unit] ?? millisecondsPerDay
55
+ start.setTime(now.getTime() - amount * duration)
56
+ }
57
+ if (Number.isFinite(start.getTime())) {
58
+ values.push({ label: match[0], value: start.toISOString(), end: now.toISOString() })
59
+ }
60
+ }
61
+ for (const match of prompt.matchAll(
62
+ /\b\d{4}-\d{2}-\d{2}(?:T\d{2}:\d{2}(?::\d{2}(?:\.\d+)?)?(?:Z|[+-]\d{2}:\d{2})?)?\b/g,
63
+ )) {
64
+ const parsed = new Date(match[0])
65
+ if (Number.isFinite(parsed.getTime())) {
66
+ values.push({
67
+ label: match[0],
68
+ value: parsed.toISOString(),
69
+ ...(match[0].length === 10
70
+ ? { end: new Date(parsed.getTime() + 86400000).toISOString() }
71
+ : {}),
72
+ })
73
+ }
74
+ }
75
+ for (const match of prompt.matchAll(/\b(?:19|20)\d{2}\b/g)) {
76
+ if (prompt.slice(match.index! + 4, match.index! + 5) === '-') {
77
+ continue
78
+ }
79
+ const y = Number(match[0])
80
+ values.push({ label: `year ${y}`, value: day(y, 0, 1), end: day(y + 1, 0, 1) })
81
+ }
82
+ return values
83
+ }
@@ -0,0 +1,50 @@
1
+ /** Closed choices, boolean flags, or literal spans copied from the prompt. */
2
+ export type ArgSpec =
3
+ | {
4
+ kind: 'choice'
5
+ question: string
6
+ stated?: string
7
+ options: Record<string, string>
8
+ }
9
+ | { kind: 'flag'; question: string }
10
+ | { kind: 'span'; question: string; stated: string }
11
+
12
+ export interface ToolSpec {
13
+ /** What the tool does, in the words a user might use. Jev routes on this. */
14
+ description: string
15
+ args: Record<string, ArgSpec>
16
+ }
17
+
18
+ export type AgentToolName = string
19
+
20
+ export type ArgValue = string | string[] | boolean
21
+
22
+ export interface ToolCallArgument {
23
+ /** Left out because the prompt did not mention it. */
24
+ omitted: boolean
25
+ value?: ArgValue
26
+ /** Normalized decision confidence, from 0 to 1. */
27
+ confidence: number
28
+ }
29
+
30
+ /** What the server answers for a prompt. `tool: null` means no tool fits. */
31
+ export interface AgentToolCall {
32
+ tool: AgentToolName | null
33
+ args: Record<string, ArgValue>
34
+ arguments: Record<string, ToolCallArgument>
35
+ /** The least certain judgement behind the call. */
36
+ confidence: number
37
+ weakest: string | null
38
+ model: string
39
+ /** Validated, read-only model query assembled from schema-derived Jev decisions. */
40
+ resource?: {
41
+ id: string
42
+ query: import('@nuxtagentic/data/runtime/shared/resource-query').ResourceQuery
43
+ }
44
+ }
45
+
46
+ export const JEV_MODELS = [
47
+ { id: 'jev-latest', name: 'Jev latest', description: 'Stable release' },
48
+ { id: 'jev-preview', name: 'Jev preview', description: 'Newest build' },
49
+ { id: 'jev-1.13.0', name: 'Jev 1.13', description: 'Pinned version' },
50
+ ] as const
@@ -0,0 +1,5 @@
1
+ import type { WorkspaceMetadata } from '@nuxtagentic/prompt/server-contributions'
2
+
3
+ export default {
4
+ excludedModels: ['agentOperation'],
5
+ } satisfies WorkspaceMetadata
@@ -0,0 +1 @@
1
+ export type DecisionProvider = 'openAI' | 'typesafeAI'
@@ -0,0 +1,27 @@
1
+ export interface InferencePolicy {
2
+ /** Total time across all stages and SDK retries. */
3
+ deadlineMs: number
4
+ /** Timeout of an individual SDK attempt. */
5
+ timeoutMs: number
6
+ maxRetries: number
7
+ }
8
+
9
+ export interface PromptConfig {
10
+ model: string
11
+ instructions: string
12
+ maxConstraintsPerField: number
13
+ candidates: { span: number; text: number; maxWords: number }
14
+ /** Bound speculative model extraction on global requests. */
15
+ routing: { maxInlineModels: number; maxInlineQuestions: number; maxInlineBytes: number }
16
+ inference: InferencePolicy
17
+ rerank: InferencePolicy & {
18
+ maxFiles: number
19
+ excerptChars: number
20
+ queryChars: number
21
+ nameChars: number
22
+ folderChars: number
23
+ mimeTypeChars: number
24
+ }
25
+ /** Log timing and usage, without prompts, candidates or answers. */
26
+ logMetrics: boolean
27
+ }
package/schema.ts ADDED
@@ -0,0 +1,69 @@
1
+ import { model, t } from '@cloudconfig/core'
2
+
3
+ export const agentOperationsModels = {
4
+ // Durable, session-bound reviews. Public CRUD cannot write any of these private fields.
5
+ agentOperation: model(
6
+ {
7
+ key: t.text().required().unique().serverOnly(),
8
+ ownerId: t.number().required().serverOnly(),
9
+ sessionId: t.text().required().serverOnly(),
10
+ expiresAt: t.datetime().required().serverOnly(),
11
+ state: t
12
+ .select(['pending', 'executing', 'completed', 'failed', 'cancelled'])
13
+ .default('pending')
14
+ .serverOnly(),
15
+ plan: t.json().required().serverOnly(),
16
+ result: t.json().serverOnly(),
17
+ version: t.number().default(0).serverOnly(),
18
+ expectedVersion: t.number().default(-1).serverOnly(),
19
+ },
20
+ {
21
+ access: {
22
+ list: ({ user, where }) => (user ? where('ownerId', 'eq', user.id) : false),
23
+ read: ({ user, where }) => (user ? where('ownerId', 'eq', user.id) : false),
24
+ create: ({ user, input }) =>
25
+ !!user && input?.ownerId === user.id && !!input?.plan && input?.state === 'pending',
26
+ update: ({ user, input, where }) =>
27
+ user && Number.isSafeInteger(input?.expectedVersion)
28
+ ? where('ownerId', 'eq', user.id)
29
+ : false,
30
+ },
31
+ validate: (input, existing) => {
32
+ const issues = []
33
+ if (
34
+ !/^[0-9a-f-]{36}$/.test(String(input.key)) ||
35
+ !String(input.sessionId) ||
36
+ !Number.isFinite(Date.parse(String(input.expiresAt))) ||
37
+ JSON.stringify(input.plan).length > 100_000
38
+ ) {
39
+ issues.push({ path: 'plan', message: 'Invalid action confirmation.' })
40
+ }
41
+ if (
42
+ existing &&
43
+ (input.expectedVersion !== existing.version ||
44
+ input.version !== Number(existing.version) + 1)
45
+ ) {
46
+ issues.push({ path: 'version', message: 'This action was already claimed or changed.' })
47
+ }
48
+ if (existing) {
49
+ const transitions: Record<string, string[]> = {
50
+ pending: ['executing', 'cancelled'],
51
+ executing: ['completed', 'failed'],
52
+ }
53
+ if (
54
+ !(transitions[String(existing.state)] ?? []).includes(String(input.state)) ||
55
+ ['key', 'ownerId', 'sessionId', 'expiresAt'].some(
56
+ (key) => input[key] !== existing[key],
57
+ ) ||
58
+ JSON.stringify(input.plan) !== JSON.stringify(existing.plan)
59
+ ) {
60
+ issues.push({ path: 'state', message: 'Invalid action confirmation transition.' })
61
+ }
62
+ }
63
+ return issues
64
+ },
65
+ },
66
+ ),
67
+ }
68
+
69
+ export type AgentOperationsConfig = { models: typeof agentOperationsModels }
@@ -0,0 +1,53 @@
1
+ import type { ClientOptions } from '@cloudconfig/core/client'
2
+ import type { H3Event } from 'h3'
3
+ import type { ModelView } from '@nuxtagentic/data/runtime/shared/model-views'
4
+ import type { ToolSpec } from './runtime/shared/agent-tools'
5
+ import type { Resource } from '@nuxtagentic/prompt/core/index'
6
+
7
+ export interface WorkspaceMetadata {
8
+ views?: readonly ModelView[]
9
+ tools?: Record<string, ToolSpec>
10
+ resourceAliases?: Record<string, readonly string[]>
11
+ excludedModels?: readonly string[]
12
+ googleServices?: Record<
13
+ string,
14
+ {
15
+ label: string
16
+ scopes: readonly string[]
17
+ disconnect?: {
18
+ dataPrefixes?: readonly string[]
19
+ userStatePrefixes?: readonly string[]
20
+ }
21
+ }
22
+ >
23
+ }
24
+
25
+ export interface AgentSession {
26
+ user: { id: string | number; [key: string]: unknown }
27
+ session: { id: string | number; activeOrganizationId?: string | number | null }
28
+ }
29
+
30
+ export type AgentResourceFactory = (context: {
31
+ event: H3Event
32
+ authBase: string
33
+ session: AgentSession
34
+ clientOptions: ClientOptions
35
+ }) => Resource | Promise<Resource>
36
+
37
+ export type GoogleDisconnectHook = (input: {
38
+ event: H3Event
39
+ service: string
40
+ user: { id: number }
41
+ }) => void | Promise<void>
42
+
43
+ export type GoogleAvailabilityHook = (input: {
44
+ event: H3Event
45
+ service: string
46
+ user: { id: number; email: string }
47
+ }) => string | undefined | Promise<string | undefined>
48
+
49
+ export interface ServerContribution extends WorkspaceMetadata {
50
+ resourceFactories?: readonly AgentResourceFactory[]
51
+ googleDisconnectHooks?: readonly GoogleDisconnectHook[]
52
+ googleAvailabilityHooks?: readonly GoogleAvailabilityHook[]
53
+ }