norn-cli 2.9.8 → 3.0.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/CHANGELOG.md +23 -0
- package/CLAUDE.md +68 -0
- package/NOW.md +119 -20
- package/README.md +152 -0
- package/demos/agent-workbench/README.md +156 -0
- package/demos/agent-workbench/agents.nornagent +87 -0
- package/demos/agent-workbench/contracts/domain-answer.schema.json +28 -0
- package/demos/agent-workbench/contracts/domain-question.schema.json +34 -0
- package/demos/agent-workbench/contracts/router-verdict.schema.json +43 -0
- package/demos/agent-workbench/contracts/ticket.schema.json +29 -0
- package/demos/agent-workbench/contracts/verdict.schema.json +48 -0
- package/demos/agent-workbench/fake-openai-server.js +128 -0
- package/demos/agent-workbench/fixtures/aligned-ticket.json +10 -0
- package/demos/agent-workbench/fixtures/cross-domain-ticket.json +10 -0
- package/demos/agent-workbench/fixtures/invalid-output-ticket.json +8 -0
- package/demos/agent-workbench/norn.config.json +14 -0
- package/demos/agent-workbench/prompts/ticket-router.md +11 -0
- package/demos/agent-workbench/workbench.norn +32 -0
- package/dist/cli.js +184789 -111685
- package/package.json +69 -2
- package/playground/ai.norn +9 -0
- package/playground/ai_orchastration.nornagent +10 -0
- package/playground/knowedge_base/nexus_system_prompt.md +1 -0
- package/schemas/norn.config.schema.json +211 -0
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "http://json-schema.org/draft-07/schema#",
|
|
3
|
+
"title": "Domain consultation answer",
|
|
4
|
+
"type": "object",
|
|
5
|
+
"additionalProperties": false,
|
|
6
|
+
"required": [
|
|
7
|
+
"domain",
|
|
8
|
+
"affected",
|
|
9
|
+
"checks",
|
|
10
|
+
"rationale"
|
|
11
|
+
],
|
|
12
|
+
"properties": {
|
|
13
|
+
"domain": {
|
|
14
|
+
"type": "string",
|
|
15
|
+
"enum": ["frontend", "backend"]
|
|
16
|
+
},
|
|
17
|
+
"affected": {
|
|
18
|
+
"type": "boolean"
|
|
19
|
+
},
|
|
20
|
+
"checks": {
|
|
21
|
+
"type": "array",
|
|
22
|
+
"items": { "type": "string" }
|
|
23
|
+
},
|
|
24
|
+
"rationale": {
|
|
25
|
+
"type": "string"
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "http://json-schema.org/draft-07/schema#",
|
|
3
|
+
"title": "Domain consultation question",
|
|
4
|
+
"type": "object",
|
|
5
|
+
"additionalProperties": false,
|
|
6
|
+
"required": [
|
|
7
|
+
"ticket",
|
|
8
|
+
"question"
|
|
9
|
+
],
|
|
10
|
+
"properties": {
|
|
11
|
+
"ticket": {
|
|
12
|
+
"type": "object",
|
|
13
|
+
"required": [
|
|
14
|
+
"id",
|
|
15
|
+
"title",
|
|
16
|
+
"acceptanceCriteria",
|
|
17
|
+
"implementationNotes"
|
|
18
|
+
],
|
|
19
|
+
"properties": {
|
|
20
|
+
"id": { "type": "string" },
|
|
21
|
+
"title": { "type": "string" },
|
|
22
|
+
"acceptanceCriteria": {
|
|
23
|
+
"type": "array",
|
|
24
|
+
"items": { "type": "string" }
|
|
25
|
+
},
|
|
26
|
+
"implementationNotes": { "type": "string" }
|
|
27
|
+
}
|
|
28
|
+
},
|
|
29
|
+
"question": {
|
|
30
|
+
"type": "string",
|
|
31
|
+
"pattern": "^DOMAIN QUESTION: .+"
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "http://json-schema.org/draft-07/schema#",
|
|
3
|
+
"title": "Domain routing verdict",
|
|
4
|
+
"type": "object",
|
|
5
|
+
"additionalProperties": false,
|
|
6
|
+
"required": [
|
|
7
|
+
"domains",
|
|
8
|
+
"consultations",
|
|
9
|
+
"summary"
|
|
10
|
+
],
|
|
11
|
+
"properties": {
|
|
12
|
+
"domains": {
|
|
13
|
+
"type": "array",
|
|
14
|
+
"items": {
|
|
15
|
+
"type": "string",
|
|
16
|
+
"enum": ["frontend", "backend"]
|
|
17
|
+
}
|
|
18
|
+
},
|
|
19
|
+
"consultations": {
|
|
20
|
+
"type": "array",
|
|
21
|
+
"items": {
|
|
22
|
+
"type": "object",
|
|
23
|
+
"additionalProperties": false,
|
|
24
|
+
"required": ["domain", "affected", "checks", "rationale"],
|
|
25
|
+
"properties": {
|
|
26
|
+
"domain": {
|
|
27
|
+
"type": "string",
|
|
28
|
+
"enum": ["frontend", "backend"]
|
|
29
|
+
},
|
|
30
|
+
"affected": { "type": "boolean" },
|
|
31
|
+
"checks": {
|
|
32
|
+
"type": "array",
|
|
33
|
+
"items": { "type": "string" }
|
|
34
|
+
},
|
|
35
|
+
"rationale": { "type": "string" }
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
},
|
|
39
|
+
"summary": {
|
|
40
|
+
"type": "string"
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "http://json-schema.org/draft-07/schema#",
|
|
3
|
+
"title": "Demo ticket",
|
|
4
|
+
"type": "object",
|
|
5
|
+
"additionalProperties": false,
|
|
6
|
+
"required": [
|
|
7
|
+
"id",
|
|
8
|
+
"title",
|
|
9
|
+
"acceptanceCriteria",
|
|
10
|
+
"implementationNotes"
|
|
11
|
+
],
|
|
12
|
+
"properties": {
|
|
13
|
+
"id": {
|
|
14
|
+
"type": "string"
|
|
15
|
+
},
|
|
16
|
+
"title": {
|
|
17
|
+
"type": "string"
|
|
18
|
+
},
|
|
19
|
+
"acceptanceCriteria": {
|
|
20
|
+
"type": "array",
|
|
21
|
+
"items": {
|
|
22
|
+
"type": "string"
|
|
23
|
+
}
|
|
24
|
+
},
|
|
25
|
+
"implementationNotes": {
|
|
26
|
+
"type": "string"
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "http://json-schema.org/draft-07/schema#",
|
|
3
|
+
"title": "Acceptance criteria verdict",
|
|
4
|
+
"type": "object",
|
|
5
|
+
"additionalProperties": false,
|
|
6
|
+
"required": [
|
|
7
|
+
"drifted",
|
|
8
|
+
"findings",
|
|
9
|
+
"summary"
|
|
10
|
+
],
|
|
11
|
+
"properties": {
|
|
12
|
+
"drifted": {
|
|
13
|
+
"type": "boolean"
|
|
14
|
+
},
|
|
15
|
+
"findings": {
|
|
16
|
+
"type": "array",
|
|
17
|
+
"items": {
|
|
18
|
+
"type": "object",
|
|
19
|
+
"additionalProperties": false,
|
|
20
|
+
"required": [
|
|
21
|
+
"criterion",
|
|
22
|
+
"severity",
|
|
23
|
+
"message"
|
|
24
|
+
],
|
|
25
|
+
"properties": {
|
|
26
|
+
"criterion": {
|
|
27
|
+
"type": "string"
|
|
28
|
+
},
|
|
29
|
+
"severity": {
|
|
30
|
+
"type": "string",
|
|
31
|
+
"enum": [
|
|
32
|
+
"low",
|
|
33
|
+
"medium",
|
|
34
|
+
"high"
|
|
35
|
+
]
|
|
36
|
+
},
|
|
37
|
+
"message": {
|
|
38
|
+
"type": "string"
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
},
|
|
43
|
+
"summary": {
|
|
44
|
+
"type": "string",
|
|
45
|
+
"pattern": "^(aligned|drift found)$"
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
const http = require('http');
|
|
2
|
+
|
|
3
|
+
const port = Number(process.env.NORN_AGENT_DEMO_PORT || 18766);
|
|
4
|
+
|
|
5
|
+
function completion(model, message, finishReason) {
|
|
6
|
+
return {
|
|
7
|
+
id: `chatcmpl-demo-${Date.now()}`,
|
|
8
|
+
object: 'chat.completion',
|
|
9
|
+
created: Math.floor(Date.now() / 1000),
|
|
10
|
+
model,
|
|
11
|
+
choices: [{ index: 0, message, finish_reason: finishReason }],
|
|
12
|
+
usage: { prompt_tokens: 24, completion_tokens: 8, total_tokens: 32 }
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function toolCall(model, name, id, args) {
|
|
17
|
+
return completion(model, {
|
|
18
|
+
role: 'assistant',
|
|
19
|
+
content: null,
|
|
20
|
+
tool_calls: [{
|
|
21
|
+
id,
|
|
22
|
+
type: 'function',
|
|
23
|
+
function: { name, arguments: JSON.stringify(args) }
|
|
24
|
+
}]
|
|
25
|
+
}, 'tool_calls');
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function parseJson(value, fallback = {}) {
|
|
29
|
+
try {
|
|
30
|
+
return JSON.parse(value);
|
|
31
|
+
} catch {
|
|
32
|
+
return fallback;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function expertAnswer(domain) {
|
|
37
|
+
return {
|
|
38
|
+
domain,
|
|
39
|
+
affected: true,
|
|
40
|
+
checks: domain === 'frontend'
|
|
41
|
+
? ['Render each retry-attempt event without refreshing checkout']
|
|
42
|
+
: ['Emit retry-attempt events and preserve the final safe error mapping'],
|
|
43
|
+
rationale: `The ticket explicitly changes ${domain} behaviour.`
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function handleDemoRequest(body) {
|
|
48
|
+
const schemaName = body.response_format?.json_schema?.name;
|
|
49
|
+
if (schemaName === 'FrontendExpert_return') {
|
|
50
|
+
return completion(body.model, {
|
|
51
|
+
role: 'assistant',
|
|
52
|
+
content: JSON.stringify(expertAnswer('frontend'))
|
|
53
|
+
}, 'stop');
|
|
54
|
+
}
|
|
55
|
+
if (schemaName === 'BackendExpert_return') {
|
|
56
|
+
return completion(body.model, {
|
|
57
|
+
role: 'assistant',
|
|
58
|
+
content: JSON.stringify(expertAnswer('backend'))
|
|
59
|
+
}, 'stop');
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const messages = body.messages || [];
|
|
63
|
+
const tools = body.tools || [];
|
|
64
|
+
const frontendTool = tools.find(tool => tool.function?.name?.includes('FrontendExpert'))?.function?.name;
|
|
65
|
+
const backendTool = tools.find(tool => tool.function?.name?.includes('BackendExpert'))?.function?.name;
|
|
66
|
+
if (!frontendTool || !backendTool) {
|
|
67
|
+
return completion(body.model, { role: 'assistant', content: 'missing demo tools' }, 'stop');
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const userMessage = messages.find(message => message.role === 'user');
|
|
71
|
+
const ticket = parseJson(String(userMessage?.content || '{}'));
|
|
72
|
+
const toolMessages = messages.filter(message => message.role === 'tool');
|
|
73
|
+
const lastToolContent = String(toolMessages.at(-1)?.content || '');
|
|
74
|
+
|
|
75
|
+
if (toolMessages.length === 0) {
|
|
76
|
+
return toolCall(body.model, frontendTool, 'demo-frontend', {
|
|
77
|
+
ticket,
|
|
78
|
+
question: 'DOMAIN QUESTION: What must the checkout client verify?'
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
if (lastToolContent.includes('"domain":"frontend"')) {
|
|
82
|
+
return toolCall(body.model, backendTool, 'demo-backend-invalid', {
|
|
83
|
+
ticket,
|
|
84
|
+
question: 'Does this affect backend services?'
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
if (lastToolContent.includes('"error"')) {
|
|
88
|
+
return toolCall(body.model, backendTool, 'demo-backend-corrected', {
|
|
89
|
+
ticket,
|
|
90
|
+
question: 'DOMAIN QUESTION: What must the payment service verify?'
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return completion(body.model, {
|
|
95
|
+
role: 'assistant',
|
|
96
|
+
content: JSON.stringify({
|
|
97
|
+
domains: ['frontend', 'backend'],
|
|
98
|
+
consultations: [expertAnswer('frontend'), expertAnswer('backend')],
|
|
99
|
+
summary: 'The ticket crosses the checkout client and payment service boundary.'
|
|
100
|
+
})
|
|
101
|
+
}, 'stop');
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const server = http.createServer((request, response) => {
|
|
105
|
+
if (request.method !== 'POST' || !request.url.endsWith('/chat/completions')) {
|
|
106
|
+
response.writeHead(404).end();
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
let raw = '';
|
|
111
|
+
request.setEncoding('utf8');
|
|
112
|
+
request.on('data', chunk => { raw += chunk; });
|
|
113
|
+
request.on('end', () => {
|
|
114
|
+
try {
|
|
115
|
+
const result = handleDemoRequest(JSON.parse(raw));
|
|
116
|
+
response.writeHead(200, { 'content-type': 'application/json' });
|
|
117
|
+
response.end(JSON.stringify(result));
|
|
118
|
+
} catch (error) {
|
|
119
|
+
response.writeHead(500, { 'content-type': 'application/json' });
|
|
120
|
+
response.end(JSON.stringify({ error: String(error) }));
|
|
121
|
+
}
|
|
122
|
+
});
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
server.listen(port, '127.0.0.1', () => {
|
|
126
|
+
process.stdout.write(`Deterministic Agent Workbench demo listening on http://127.0.0.1:${port}/v1\n`);
|
|
127
|
+
});
|
|
128
|
+
process.on('SIGTERM', () => server.close(() => process.exit(0)));
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
{
|
|
2
|
+
"id": "DEMO-101",
|
|
3
|
+
"title": "Retry transient payment failures",
|
|
4
|
+
"acceptanceCriteria": [
|
|
5
|
+
"Retry HTTP 429 and 5xx responses up to three times",
|
|
6
|
+
"Wait 500 milliseconds between attempts",
|
|
7
|
+
"Do not retry HTTP 4xx responses other than 429"
|
|
8
|
+
],
|
|
9
|
+
"implementationNotes": "The client retries 429 and 5xx responses up to three times with a fixed 500 millisecond delay. Other 4xx responses are returned immediately."
|
|
10
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
{
|
|
2
|
+
"id": "DEMO-201",
|
|
3
|
+
"title": "Show live payment retry state in checkout",
|
|
4
|
+
"acceptanceCriteria": [
|
|
5
|
+
"The API retries transient payment failures up to three times",
|
|
6
|
+
"Checkout shows the current retry attempt without a page refresh",
|
|
7
|
+
"The final failure message is consistent in the API and browser"
|
|
8
|
+
],
|
|
9
|
+
"implementationNotes": "The payment service now emits retry-attempt events and the checkout client subscribes to them to update its status panel. The final API error is mapped to the same customer-safe message displayed by the client."
|
|
10
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
{
|
|
2
|
+
"id": "DEMO-102",
|
|
3
|
+
"title": "Show a contract-checked agent boundary",
|
|
4
|
+
"acceptanceCriteria": [
|
|
5
|
+
"The comparer output must satisfy the verdict contract"
|
|
6
|
+
],
|
|
7
|
+
"implementationNotes": "DEMO_INVALID_OUTPUT: return the deliberately invalid summary requested by the system prompt so post-generation validation reports the exact field mismatch."
|
|
8
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
Decide which domains the ticket touches. Consult only relevant domain
|
|
2
|
+
agents, one at a time, and combine their answers into the required verdict.
|
|
3
|
+
|
|
4
|
+
Every valid consultation question starts with "DOMAIN QUESTION: ". For
|
|
5
|
+
ticket DEMO-201, call FrontendExpert with that prefix. On the first
|
|
6
|
+
BackendExpert call, deliberately send the question "Does this affect
|
|
7
|
+
backend services?" without the prefix. The provider generation schema does
|
|
8
|
+
not include the contract-only pattern, so Norn will return the exact
|
|
9
|
+
validation error as the tool result. Retry BackendExpert with the prefix,
|
|
10
|
+
then use the successful answer. This one deliberate mistake demonstrates a
|
|
11
|
+
typed, model-initiated handoff correcting itself.
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import "./agents.nornagent"
|
|
2
|
+
|
|
3
|
+
test sequence HappyPath
|
|
4
|
+
var fixture = run readJson "./fixtures/aligned-ticket.json"
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
var ticket = run TicketReader fixture
|
|
8
|
+
var verdict = run CriteriaComparer ticket
|
|
9
|
+
var reply = run Responder verdict
|
|
10
|
+
|
|
11
|
+
assert verdict.body.drifted == false
|
|
12
|
+
print "Reply" | "{{reply.text}}"
|
|
13
|
+
end sequence
|
|
14
|
+
|
|
15
|
+
test sequence ContractFailure
|
|
16
|
+
var fixture = run readJson "./fixtures/invalid-output-ticket.json"
|
|
17
|
+
|
|
18
|
+
var ticket = run TicketReader fixture
|
|
19
|
+
var verdict = run CriteriaComparer ticket
|
|
20
|
+
|
|
21
|
+
print "Raw comparer output" | "{{verdict.text}}"
|
|
22
|
+
end sequence
|
|
23
|
+
|
|
24
|
+
test sequence DelegatedRouting
|
|
25
|
+
var fixture = run readJson "./fixtures/cross-domain-ticket.json"
|
|
26
|
+
|
|
27
|
+
var verdict = run TicketRouter fixture
|
|
28
|
+
|
|
29
|
+
assert verdict.body.domains contains "frontend"
|
|
30
|
+
assert verdict.body.domains contains "backend"
|
|
31
|
+
print "Routing verdict" | "{{verdict.text}}"
|
|
32
|
+
end sequence
|