@frontera-sdk/cli 1.52.47 → 1.53.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.
- package/package.json +4 -4
- package/src/local/local-operating.ts +106 -11
- package/src/local/local-scaffold.ts +2 -2
- package/src/local/local-schemas.ts +22 -0
- package/src/vendor/sdk-sources.json +17 -17
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@frontera-sdk/cli",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.53.0",
|
|
4
4
|
"description": "The frontera CLI — scaffold, pull, save and deploy Frontera apps and automations.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"frontera",
|
|
@@ -40,8 +40,8 @@
|
|
|
40
40
|
"dependencies": {
|
|
41
41
|
"@anthropic-ai/claude-agent-sdk": "^0.3.251",
|
|
42
42
|
"@faker-js/faker": "10.6.0",
|
|
43
|
-
"@frontera-sdk/core": "1.
|
|
44
|
-
"@frontera-sdk/functions": "1.
|
|
43
|
+
"@frontera-sdk/core": "1.53.0",
|
|
44
|
+
"@frontera-sdk/functions": "1.53.0",
|
|
45
45
|
"ai": "^6.0.116",
|
|
46
46
|
"axe-core": "4.12.1",
|
|
47
47
|
"gray-matter": "^4.0.3",
|
|
@@ -49,7 +49,7 @@
|
|
|
49
49
|
"yaml": "^2.9.0"
|
|
50
50
|
},
|
|
51
51
|
"devDependencies": {
|
|
52
|
-
"@frontera-sdk/forge-contracts": "1.
|
|
52
|
+
"@frontera-sdk/forge-contracts": "1.53.0",
|
|
53
53
|
"@types/bun": "^1.3.14",
|
|
54
54
|
"typescript": "^5.9.3"
|
|
55
55
|
}
|
|
@@ -4,7 +4,7 @@ import type { LocalCredentials } from './local-state'
|
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
6
|
* An Agent's behavior settings (`agents/<slug>/operating.json`: harness,
|
|
7
|
-
* procedures, variables, focus, reply checks) staged onto its draft.
|
|
7
|
+
* procedures, variables, focus, reply checks, handover reasons) staged onto its draft.
|
|
8
8
|
*
|
|
9
9
|
* There is no CLI command for these: the route takes a person's session, not
|
|
10
10
|
* an API key. The local runtime has the owner's session, as Studio does, so
|
|
@@ -31,6 +31,26 @@ interface Issue {
|
|
|
31
31
|
message?: string
|
|
32
32
|
}
|
|
33
33
|
|
|
34
|
+
const isRecord = (v: unknown): v is Record<string, unknown> => !!v && typeof v === 'object' && !Array.isArray(v)
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* The keys of the handover reasons an Agent has while its operating.json sets
|
|
38
|
+
* none, as the service's `DEFAULT_HANDOVER_REASONS` (`handover-reasons.ts`)
|
|
39
|
+
* holds them: what a handoff action's `handoverReason` may name then.
|
|
40
|
+
*/
|
|
41
|
+
export const DEFAULT_HANDOVER_REASON_KEYS = ['asked_for_person', 'cannot_answer', 'needs_action', 'other'] as const
|
|
42
|
+
|
|
43
|
+
/** The reason keys a saved configuration offers: its own, or the defaults. */
|
|
44
|
+
function reasonKeys(config: unknown): string[] {
|
|
45
|
+
const own = isRecord(config) && Array.isArray(config.handoverReasons) ? (config.handoverReasons as unknown[]).flatMap((r) => (isRecord(r) && typeof r.key === 'string' ? [r.key] : [])) : []
|
|
46
|
+
return own.length ? own : [...DEFAULT_HANDOVER_REASON_KEYS]
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** What to do about a refused `handoverReasons` list, said once after its problems. */
|
|
50
|
+
function handoverReasonsFix(slug: string): string {
|
|
51
|
+
return ` Fix: in agents/${slug}/operating.json give "handoverReasons" 1 to 15 reasons, each { "key", "name", "whenToUse" } with a unique lower_snake_case key (at most 40 characters), a name of 1 to 60 characters and whenToUse of 1 to 300, one of them with the key "other"; or leave "handoverReasons" out for the default reasons. \`frontera local schema operating\` gives the format`
|
|
52
|
+
}
|
|
53
|
+
|
|
34
54
|
/**
|
|
35
55
|
* The issues a saved draft still has, one `warning:` line each, named by the
|
|
36
56
|
* project file and the setting's path
|
|
@@ -39,31 +59,104 @@ interface Issue {
|
|
|
39
59
|
* with the warnings and refuses publishing, so it is said too, as that.
|
|
40
60
|
*/
|
|
41
61
|
export function operatingIssueLines(slug: string, body: unknown): string[] {
|
|
42
|
-
const data = ((body as { data?: unknown } | null)?.data ?? body) as { issues?: unknown } | null
|
|
62
|
+
const data = ((body as { data?: unknown } | null)?.data ?? body) as { issues?: unknown; draft?: unknown } | null
|
|
43
63
|
const issues = Array.isArray(data?.issues) ? (data.issues as Issue[]) : []
|
|
44
64
|
return issues.map((issue) => {
|
|
45
65
|
const path = typeof issue.path === 'string' ? issue.path : ''
|
|
46
66
|
// The Agent's instructions are its agent.json prompts, not its behavior settings.
|
|
47
67
|
const file = path === 'instructions' || path.startsWith('instructions.') ? `agents/${slug}/agent.json` : `agents/${slug}/operating.json`
|
|
48
68
|
const blocks = issue.severity === 'error' ? ' (publishing is refused until this is fixed)' : ''
|
|
49
|
-
|
|
69
|
+
// A handoff action naming a reason the Agent doesn't have: say which keys it has.
|
|
70
|
+
const fix = issue.code === 'unknown_handover_reason'
|
|
71
|
+
? `; set "handoverReason" to one of ${reasonKeys(data?.draft).map((k) => `"${k}"`).join(', ')}, or add the reason to "handoverReasons"`
|
|
72
|
+
: ''
|
|
73
|
+
return ` warning: ${file}${path ? ` ${path}` : ''}: ${issue.message ?? issue.code ?? 'see the Agent’s settings'}${fix}${blocks}`
|
|
50
74
|
})
|
|
51
75
|
}
|
|
52
76
|
|
|
53
|
-
function describeError(status: number, body: unknown): string {
|
|
77
|
+
function describeError(status: number, body: unknown, slug?: string): string {
|
|
54
78
|
const b = (body ?? {}) as { message?: string; code?: string; details?: unknown; issues?: unknown }
|
|
55
79
|
const lines = [`error [${b.code ?? status}]: ${b.message ?? 'the behavior settings were refused'}`]
|
|
56
80
|
const issues = (b.details as { issues?: unknown } | undefined)?.issues ?? b.issues ?? b.details
|
|
57
81
|
if (Array.isArray(issues)) {
|
|
58
|
-
|
|
59
|
-
const i = issue as { path?: unknown
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
82
|
+
const paths = issues.map((issue) => {
|
|
83
|
+
const i = issue as { path?: unknown }
|
|
84
|
+
return Array.isArray(i.path) ? i.path.join('.') : typeof i.path === 'string' ? i.path : ''
|
|
85
|
+
})
|
|
86
|
+
issues.slice(0, 6).forEach((issue, n) => {
|
|
87
|
+
const path = paths[n]!
|
|
88
|
+
lines.push(` ${path ? `${path}: ` : ''}${(issue as { message?: string }).message ?? JSON.stringify(issue)}`)
|
|
89
|
+
})
|
|
90
|
+
// The rules of the handover reasons, said once: the service's words name one problem each, not the whole rule.
|
|
91
|
+
if (slug && paths.some((p) => p === 'handoverReasons' || p.startsWith('handoverReasons.'))) lines.push(handoverReasonsFix(slug))
|
|
63
92
|
}
|
|
64
93
|
return lines.join('\n')
|
|
65
94
|
}
|
|
66
95
|
|
|
96
|
+
/**
|
|
97
|
+
* The settings the file has that the saved draft does not: a runtime on a
|
|
98
|
+
* release older than the file's format keeps only the keys it knows and
|
|
99
|
+
* drops the rest without an error. Each is a warning that says it is left
|
|
100
|
+
* out here and how to run the release that has it. The keys checked are the
|
|
101
|
+
* top-level settings and each action's `handoverReason`; nothing is said when
|
|
102
|
+
* the answer carries no draft to compare.
|
|
103
|
+
*/
|
|
104
|
+
export function droppedSettingLines(slug: string, sent: unknown, saved: unknown): string[] {
|
|
105
|
+
if (!isRecord(sent) || !isRecord(saved)) return []
|
|
106
|
+
const file = `agents/${slug}/operating.json`
|
|
107
|
+
const next = 'to use it here, run the runtime on the current release: `frontera local stop`, then `frontera local start`'
|
|
108
|
+
// Settings the service retired and drops on purpose (`dropRetiredKeys`).
|
|
109
|
+
const retired = new Set(['audience', 'conversation'])
|
|
110
|
+
const out = Object.keys(sent)
|
|
111
|
+
// `$schema` and other editor keys are not settings.
|
|
112
|
+
.filter((key) => !key.startsWith('$') && !retired.has(key) && !(key in saved))
|
|
113
|
+
.map((key) => key === 'handoverReasons'
|
|
114
|
+
? ` warning: ${file} handoverReasons: this runtime's release has no handover reasons, so it left them out: here the Agent hands over without them; they still apply where the Agent runs on a release that has them. To use them here, run the runtime on the current release: \`frontera local stop\`, then \`frontera local start\``
|
|
115
|
+
: ` warning: ${file} ${key}: this runtime's format has no \`${key}\`, so it is left out here: check the name against \`frontera local schema operating\`, or, for a setting of a newer release, ${next}`)
|
|
116
|
+
const sentProcedures = Array.isArray(sent.procedures) ? sent.procedures : []
|
|
117
|
+
const savedProcedures = Array.isArray(saved.procedures) ? saved.procedures : []
|
|
118
|
+
sentProcedures.forEach((p, i) => {
|
|
119
|
+
const kept = savedProcedures[i]
|
|
120
|
+
if (!isRecord(p) || !isRecord(kept)) return
|
|
121
|
+
if (Array.isArray(p.actions) && Array.isArray(kept.actions)) {
|
|
122
|
+
p.actions.forEach((a, j) => {
|
|
123
|
+
const k = (kept.actions as unknown[])[j]
|
|
124
|
+
if (isRecord(a) && typeof a.handoverReason === 'string' && isRecord(k) && !('handoverReason' in k)) {
|
|
125
|
+
out.push(` warning: ${file} procedures.${i}.actions.${j}.handoverReason: this runtime's release has no handover reasons, so the action hands over without "${a.handoverReason}" here; ${next}`)
|
|
126
|
+
}
|
|
127
|
+
})
|
|
128
|
+
}
|
|
129
|
+
// An End step that hands over, wherever it sits in the procedure's steps.
|
|
130
|
+
const endSteps = (sentSteps: unknown, keptSteps: unknown, path: string) => {
|
|
131
|
+
if (!Array.isArray(sentSteps) || !Array.isArray(keptSteps)) return
|
|
132
|
+
sentSteps.forEach((s, j) => {
|
|
133
|
+
const k = keptSteps[j]
|
|
134
|
+
if (!isRecord(s) || !isRecord(k)) return
|
|
135
|
+
const at = `${path}.${j}`
|
|
136
|
+
if (s.type === 'end' && typeof s.handoverReason === 'string' && !('handoverReason' in k)) {
|
|
137
|
+
out.push(` warning: ${file} ${at}.handoverReason: this runtime's release has no handover reasons, so the step hands over without "${s.handoverReason}" here; ${next}`)
|
|
138
|
+
}
|
|
139
|
+
endSteps(s.onFailure, k.onFailure, `${at}.onFailure`)
|
|
140
|
+
endSteps(s.otherwise, k.otherwise, `${at}.otherwise`)
|
|
141
|
+
if (Array.isArray(s.arms) && Array.isArray(k.arms)) {
|
|
142
|
+
s.arms.forEach((arm, ai) => {
|
|
143
|
+
const keptArm = (k.arms as unknown[])[ai]
|
|
144
|
+
if (isRecord(arm) && isRecord(keptArm)) endSteps(arm.steps, keptArm.steps, `${at}.arms.${ai}.steps`)
|
|
145
|
+
})
|
|
146
|
+
}
|
|
147
|
+
})
|
|
148
|
+
}
|
|
149
|
+
endSteps(p.steps, kept.steps, `procedures.${i}.steps`)
|
|
150
|
+
if (Array.isArray(p.blocks) && Array.isArray(kept.blocks)) {
|
|
151
|
+
p.blocks.forEach((b, bi) => {
|
|
152
|
+
const keptBlock = (kept.blocks as unknown[])[bi]
|
|
153
|
+
if (isRecord(b) && isRecord(keptBlock)) endSteps(b.steps, keptBlock.steps, `procedures.${i}.blocks.${bi}.steps`)
|
|
154
|
+
})
|
|
155
|
+
}
|
|
156
|
+
})
|
|
157
|
+
return out
|
|
158
|
+
}
|
|
159
|
+
|
|
67
160
|
export async function applyOperatingConfig(args: {
|
|
68
161
|
serviceUrl: string
|
|
69
162
|
creds: LocalCredentials
|
|
@@ -111,9 +204,11 @@ export async function applyOperatingConfig(args: {
|
|
|
111
204
|
}
|
|
112
205
|
if (agent.status !== 200 || !id) return { ok: false, output: describeError(agent.status, agent.body) }
|
|
113
206
|
const put = await call(`/v1/config/agents/${id}/operating`, { method: 'PUT', body: { operatingConfig } })
|
|
114
|
-
if (put.status >= 400) return { ok: false, output: describeError(put.status, put.body) }
|
|
207
|
+
if (put.status >= 400) return { ok: false, output: describeError(put.status, put.body, args.slug) }
|
|
115
208
|
const warnings = operatingIssueLines(args.slug, put.body)
|
|
116
|
-
|
|
209
|
+
// What this runtime's release left out is an adaptation to it, not a finding on the file.
|
|
210
|
+
const dropped = droppedSettingLines(args.slug, operatingConfig, (put.body as { data?: { draft?: unknown } } | null)?.data?.draft)
|
|
211
|
+
return { ok: true, output: [...warnings, ...dropped].join('\n'), ...(warnings.length ? { validationWarnings: warnings } : {}) }
|
|
117
212
|
} catch (err) {
|
|
118
213
|
return { ok: false, output: `error [UNAVAILABLE]: the local service did not answer: ${(err as Error).message}` }
|
|
119
214
|
}
|
|
@@ -176,7 +176,7 @@ A Frontera project. Every file here is applied to a local runtime by
|
|
|
176
176
|
| Path | What it is |
|
|
177
177
|
|---|---|
|
|
178
178
|
| \`agents/<slug>/agent.json\` | An Agent's draft: model, instructions, tools |
|
|
179
|
-
| \`agents/<slug>/operating.json\` | Its behavior: procedures, variables, focus, reply checks |
|
|
179
|
+
| \`agents/<slug>/operating.json\` | Its behavior: procedures, variables, focus, reply checks, handover reasons |
|
|
180
180
|
| \`agents/<slug>/bindings.json\` | What it may use: Functions, workflows, Blueprint object types |
|
|
181
181
|
| \`agents/<slug>/evals/*.yaml\` | Its test cases |
|
|
182
182
|
| \`workflows/<slug>.yaml\` | A workflow |
|
|
@@ -301,7 +301,7 @@ Frontera deployment.
|
|
|
301
301
|
| Path | What it is |
|
|
302
302
|
|---|---|
|
|
303
303
|
| \`agents/<slug>/agent.json\` | An Agent's draft: model, instructions, tools |
|
|
304
|
-
| \`agents/<slug>/operating.json\` | Its behavior: procedures, variables, focus, reply checks |
|
|
304
|
+
| \`agents/<slug>/operating.json\` | Its behavior: procedures, variables, focus, reply checks, handover reasons |
|
|
305
305
|
| \`agents/<slug>/bindings.json\` | What it may use: the Functions and workflows it may start, the Blueprint object types it may query |
|
|
306
306
|
| \`agents/<slug>/evals/*.yaml\` | Its test cases: a conversation and what a good reply does |
|
|
307
307
|
| \`workflows/<slug>.yaml\` | A workflow |
|
|
@@ -6,6 +6,7 @@ import { BLUEPRINT_SCHEMA_FILES, BLUEPRINT_SCHEMAS } from './local-blueprint-sch
|
|
|
6
6
|
import type { LocalProject } from './local-project'
|
|
7
7
|
import { isAlive, readState } from './local-state'
|
|
8
8
|
import { PROCEDURE_TOOL_FORMS, PROCEDURE_TOOL_GROUPS } from './local-agent-checks'
|
|
9
|
+
import { DEFAULT_HANDOVER_REASON_KEYS } from './local-operating'
|
|
9
10
|
import { REF_KINDS, refPattern, type RefKind } from './local-sync-refs'
|
|
10
11
|
|
|
11
12
|
/**
|
|
@@ -888,6 +889,27 @@ function describeProjectFields(props: Record<string, unknown>): void {
|
|
|
888
889
|
if ('dataScopes' in props && 'variables' in props) {
|
|
889
890
|
describe(props, 'dataScopes', 'Optional. Narrows what the Agent reads of an object type to the records of the conversation: `[{ "objectType": "WorkOrder", "property": "customerId", "equals": "{{customer_id}}" }]`, at most one per object type. Every read of that type (query, fetch, aggregate, saved sets, links, media and metrics) sees only rows whose property equals the conversation variable; a conversation without the variable sees none. The variable is one in `variables`, and the property and variable are text or numbers. The object type must be granted in bindings.json `blueprint`. In an eval case, `variables` gives the conversation its values.')
|
|
890
891
|
}
|
|
892
|
+
// Behavior settings (operating.json): why the Agent hands a conversation to people.
|
|
893
|
+
if ('handoverReasons' in props && 'variables' in props) {
|
|
894
|
+
const reasons = props.handoverReasons
|
|
895
|
+
const item = isRecord(reasons) && isRecord(reasons.items) ? reasons.items : null
|
|
896
|
+
if (isRecord(reasons) && item && isRecord(item.properties)) {
|
|
897
|
+
const inner = { ...item.properties }
|
|
898
|
+
describe(inner, 'key', 'The reason’s id: lower snake case (`order_problem`), at most 40 characters, unique in the list. Hand-over reports count by it, so keep it once the Agent is published; a handoff action names it in `handoverReason`.')
|
|
899
|
+
describe(inner, 'name', 'What people see in hand-over reports and the inbox (`Order problem`), 1 to 60 characters.')
|
|
900
|
+
describe(inner, 'whenToUse', 'What the Agent reads to choose this reason (`The customer reports a late, wrong or damaged order.`), 1 to 300 characters.')
|
|
901
|
+
props.handoverReasons = { ...reasons, items: { ...item, properties: inner } }
|
|
902
|
+
}
|
|
903
|
+
describe(props, 'handoverReasons', `Optional. Why the Agent hands a conversation to people; hand-over reports count by these. 1 to 15 reasons, each \`{ "key", "name", "whenToUse" }\`, keys unique, names unique ignoring case, and one must have the key \`other\` (rename it, never remove it). Leave the key out to use the default reasons (${DEFAULT_HANDOVER_REASON_KEYS.map((k) => `\`${k}\``).join(', ')}); a pull writes it only when the Agent has its own. \`frontera local dev\` refuses a list that breaks these rules, with each problem's path.`)
|
|
904
|
+
}
|
|
905
|
+
// A procedure's action (operating.json `procedures[].actions[]`): the reason a handoff records.
|
|
906
|
+
if ('handoverReason' in props && 'requiresFields' in props) {
|
|
907
|
+
describe(props, 'handoverReason', 'Optional, for a `handoff` action: the `key` of one of the Agent’s `handoverReasons` (the defaults when the file has none), recorded on the hand-over when a Step by step procedure runs the action. Absent: `other`. A key the Agent doesn’t have is reported by `frontera local dev` and refuses publishing.')
|
|
908
|
+
}
|
|
909
|
+
// A Step by step End step (operating.json `procedures[].blocks[].steps[]`): the reason one that hands over records.
|
|
910
|
+
if ('handoverReason' in props && 'outcome' in props) {
|
|
911
|
+
describe(props, 'handoverReason', 'Optional, for an End step with `outcome` `handoff`: the `key` of one of the Agent’s `handoverReasons` (the defaults when the file has none), recorded on the hand-over. Absent: `other`. A key the Agent doesn’t have is reported by `frontera local dev` and refuses publishing.')
|
|
912
|
+
}
|
|
891
913
|
// An Agent (agent.json): the lists that name other resources.
|
|
892
914
|
if ('modelConfigs' in props && 'knowledgeAttachments' in props) {
|
|
893
915
|
describe(props, 'knowledgeAttachments', 'The knowledge bases the Agent searches: `[{ "knowledgeConfigId": "knowledge/<name>" }]`, one per base in knowledge/<name>/knowledge.json (`retrievalOverride` optional). `local dev` resolves each to this runtime’s id. When the key is present it is the Agent’s whole list (a base it leaves out is detached); leave the key out to keep the attachments made with `frontera knowledge attach`.', {
|