norn-cli 3.3.0 → 4.0.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.
- package/dist/cli.js +1396 -276
- package/dist/knowledgeIndexWorker.js +71 -35
- package/package.json +3 -3
- package/schemas/norn.config.schema.json +13 -0
- package/CLAUDE.md +0 -69
- package/NOW.md +0 -223
- package/demos/agent-workbench/README.md +0 -156
- package/demos/agent-workbench/agents.nornagent +0 -87
- package/demos/agent-workbench/contracts/domain-answer.schema.json +0 -28
- package/demos/agent-workbench/contracts/domain-question.schema.json +0 -34
- package/demos/agent-workbench/contracts/router-verdict.schema.json +0 -43
- package/demos/agent-workbench/contracts/ticket.schema.json +0 -29
- package/demos/agent-workbench/contracts/verdict.schema.json +0 -48
- package/demos/agent-workbench/fake-openai-server.js +0 -128
- package/demos/agent-workbench/fixtures/aligned-ticket.json +0 -10
- package/demos/agent-workbench/fixtures/cross-domain-ticket.json +0 -10
- package/demos/agent-workbench/fixtures/invalid-output-ticket.json +0 -8
- package/demos/agent-workbench/norn.config.json +0 -15
- package/demos/agent-workbench/prompts/ticket-router.md +0 -11
- package/demos/agent-workbench/workbench.norn +0 -32
- package/demos/knowledge-editor/README.md +0 -74
- package/demos/knowledge-editor/agents.nornagent +0 -16
- package/demos/knowledge-editor/api.nornapi +0 -10
- package/demos/knowledge-editor/handbook/ops/flow.svg +0 -1
- package/demos/knowledge-editor/handbook/ops/retired.md +0 -9
- package/demos/knowledge-editor/handbook/ops/runbook.md +0 -14
- package/demos/knowledge-editor/handbook/payments/charges.md +0 -25
- package/demos/knowledge-editor/handbook/payments/pricing-tiers.draft.md +0 -7
- package/demos/knowledge-editor/handbook/payments/refunds/policy.md +0 -12
- package/demos/knowledge-editor/support.norn +0 -10
- package/demos/mcp-ticket-testing/README.md +0 -114
- package/demos/mcp-ticket-testing/agents.nornagent +0 -77
- package/demos/mcp-ticket-testing/contracts/test-run.schema.json +0 -31
- package/demos/mcp-ticket-testing/expectations/proj-142.md +0 -12
- package/demos/mcp-ticket-testing/fixtures/proj-142.json +0 -13
- package/demos/mcp-ticket-testing/prompts/backend-tester.md +0 -12
- package/demos/mcp-ticket-testing/prompts/frontend-tester.md +0 -14
- package/demos/mcp-ticket-testing/prompts/reporter.md +0 -10
- package/demos/mcp-ticket-testing/servers/browser-server.js +0 -133
- package/demos/mcp-ticket-testing/servers/house-server.js +0 -125
- package/demos/mcp-ticket-testing/tickets.norn +0 -32
- package/demos/nornenv-region-refactor/README.md +0 -64
- package/demos/nornenv-showcase/README.md +0 -62
- package/demos/nornenv-showcase/norn.config.json +0 -16
- package/demos/nornenv-showcase/showcase.norn +0 -70
- package/demos/nornenv-showcase/showcase.nornapi +0 -26
- package/demos/nornenv-showcase/showcase.nornsql +0 -20
- package/demos/tests-showcase/01-single-requests.norn +0 -31
- package/demos/tests-showcase/02-sequences.norn +0 -54
- package/demos/tests-showcase/03-sidecars.norn +0 -42
- package/demos/tests-showcase/04-api-plus-sql.norn +0 -27
- package/demos/tests-showcase/db/testDb.nornsql +0 -12
- package/demos/tests-showcase/demo-api.nornapi +0 -17
- package/demos/tests-showcase/norn.config.json +0 -16
- package/playground/ai.norn +0 -15
- package/playground/ai_orchastration.nornagent +0 -34
- package/playground/knowedge_base/ACDC/testing_notes_new_clients.md +0 -11
- package/playground/knowedge_base/nexus_system_prompt.md +0 -1
|
@@ -1,156 +0,0 @@
|
|
|
1
|
-
# Agent Workbench Slice 2
|
|
2
|
-
|
|
3
|
-
This demo includes the Slice 1 linear graph and a Slice 2 model-driven graph.
|
|
4
|
-
`TicketRouter` decides which domain experts to invoke as tools, while Norn
|
|
5
|
-
contract-checks both sides of every handoff. It uses no MCP server. The checked-in
|
|
6
|
-
model declaration uses OpenAI, but you can swap it to Google, Anthropic, or an
|
|
7
|
-
OpenAI-compatible local runtime (subject to each model's tool/structured-output
|
|
8
|
-
support).
|
|
9
|
-
|
|
10
|
-
The checked-in [`norn.config.json`](./norn.config.json) spells out the runtime
|
|
11
|
-
defaults used by the demo: 16,000 output tokens for hosted providers, 4,096 for
|
|
12
|
-
`local`, depth 5, 25 total invocations, and 2 failed contract attempts.
|
|
13
|
-
Agent recording is enabled by default and keeps the latest 20 artifacts under
|
|
14
|
-
`.norn-cache/runs/`.
|
|
15
|
-
|
|
16
|
-
## Run it
|
|
17
|
-
|
|
18
|
-
Build the local CLI, export a key, then run the local build:
|
|
19
|
-
|
|
20
|
-
```bash
|
|
21
|
-
npm run compile
|
|
22
|
-
export OPENAI_API_KEY="your-key"
|
|
23
|
-
node ./dist/cli.js demos/agent-workbench/workbench.norn --sequence HappyPath
|
|
24
|
-
```
|
|
25
|
-
|
|
26
|
-
The variable name is not a convention Norn assumes — it is written in the model
|
|
27
|
-
block in [agents.nornagent](./agents.nornagent) as `apiKey {{$env.OPENAI_API_KEY}}`,
|
|
28
|
-
so any name works as long as the file and the environment agree.
|
|
29
|
-
|
|
30
|
-
Alternatively, store `OPENAI_API_KEY` as a secret under `[env:openai]` in a
|
|
31
|
-
local `.nornenv`. Norn gives the selected `.nornenv` values precedence over
|
|
32
|
-
process environment variables; add `--env openai` to the commands in that case.
|
|
33
|
-
|
|
34
|
-
`HappyPath` runs `TicketReader -> CriteriaComparer -> Responder`. The first two
|
|
35
|
-
agents produce structured output; `accepts` checks each incoming handoff and
|
|
36
|
-
`returns` checks each outgoing handoff. The final result remains available to
|
|
37
|
-
ordinary Norn assertions and interpolation through `verdict.body` and
|
|
38
|
-
`reply.text`.
|
|
39
|
-
|
|
40
|
-
## Demonstrate model-driven delegation and correction
|
|
41
|
-
|
|
42
|
-
```bash
|
|
43
|
-
node ./dist/cli.js demos/agent-workbench/workbench.norn --sequence DelegatedRouting
|
|
44
|
-
```
|
|
45
|
-
|
|
46
|
-
`TicketRouter` consults `FrontendExpert` and `BackendExpert`. Its prompt is the
|
|
47
|
-
longest in the graph, so it lives in
|
|
48
|
-
[`prompts/ticket-router.md`](./prompts/ticket-router.md) and is pulled in with
|
|
49
|
-
`system file prompts/ticket-router.md`. The file's text is the prompt verbatim,
|
|
50
|
-
which is why the quoted instructions there need no escaping. For the checked-in
|
|
51
|
-
`DEMO-201` fixture, its instructions deliberately make the first backend
|
|
52
|
-
question omit the contract-only `DOMAIN QUESTION:` prefix. That `pattern` is
|
|
53
|
-
removed from the provider's generation schema but remains in Norn's untouched
|
|
54
|
-
validation contract, so the calling model receives the exact field-level error,
|
|
55
|
-
retries with conforming input, and completes. The human trace shows the nested
|
|
56
|
-
agents and `retry 1/2 — model corrected the call`; `--json` preserves the same
|
|
57
|
-
graph under each hop's `children` array.
|
|
58
|
-
|
|
59
|
-
For a deterministic, no-key proof of the exact violate → correct → pass
|
|
60
|
-
trace, start the checked-in OpenAI-compatible demo server in one terminal:
|
|
61
|
-
|
|
62
|
-
```bash
|
|
63
|
-
node demos/agent-workbench/fake-openai-server.js
|
|
64
|
-
```
|
|
65
|
-
|
|
66
|
-
Then run the normal demo from another terminal:
|
|
67
|
-
|
|
68
|
-
```bash
|
|
69
|
-
OPENAI_BASE_URL=http://127.0.0.1:18766/v1 \
|
|
70
|
-
OPENAI_API_KEY=demo \
|
|
71
|
-
node ./dist/cli.js demos/agent-workbench/workbench.norn --sequence DelegatedRouting
|
|
72
|
-
```
|
|
73
|
-
|
|
74
|
-
The fake server is only a deterministic demo fixture. Normal runs continue to
|
|
75
|
-
use the model declared in `agents.nornagent` and the provider credentials below.
|
|
76
|
-
|
|
77
|
-
To inspect the machine-readable run artifact:
|
|
78
|
-
|
|
79
|
-
```bash
|
|
80
|
-
node ./dist/cli.js demos/agent-workbench/workbench.norn --sequence DelegatedRouting --json
|
|
81
|
-
```
|
|
82
|
-
|
|
83
|
-
After either run, replay its newest `.norn-cache/runs/*.json` artifact without
|
|
84
|
-
the fake server, an API key, or any model/tool side effects:
|
|
85
|
-
|
|
86
|
-
```bash
|
|
87
|
-
node ./dist/cli.js replay .norn-cache/runs/<recording>.json
|
|
88
|
-
node ./dist/cli.js replay .norn-cache/runs/<recording>.json --from BackendExpert
|
|
89
|
-
```
|
|
90
|
-
|
|
91
|
-
The first command is fully offline. The second replays everything before the
|
|
92
|
-
selected hop and then switches to the currently configured live provider from
|
|
93
|
-
that hop onward.
|
|
94
|
-
|
|
95
|
-
## Demonstrate a broken handoff
|
|
96
|
-
|
|
97
|
-
```bash
|
|
98
|
-
node ./dist/cli.js demos/agent-workbench/workbench.norn --sequence ContractFailure
|
|
99
|
-
```
|
|
100
|
-
|
|
101
|
-
The failure fixture asks `CriteriaComparer` for the summary `deliberately
|
|
102
|
-
invalid`, while the untouched contract permits only `aligned` or `drift found`.
|
|
103
|
-
The provider-generation schema omits the unsupported `pattern` constraint, but
|
|
104
|
-
Norn then validates the parsed output against the full schema. The run should
|
|
105
|
-
therefore fail at `summary`, retain the raw model text, and exit non-zero.
|
|
106
|
-
|
|
107
|
-
Model output is nondeterministic. If the model declines the deliberate bad-output
|
|
108
|
-
instruction, rerun the failure sequence or use the injected mock adapter in a
|
|
109
|
-
focused runtime test for deterministic automation.
|
|
110
|
-
|
|
111
|
-
## Swap the model
|
|
112
|
-
|
|
113
|
-
Edit the one `model Workbench` block in
|
|
114
|
-
[agents.nornagent](./agents.nornagent). Every agent references the alias, so the
|
|
115
|
-
whole graph moves together:
|
|
116
|
-
|
|
117
|
-
```nornagent
|
|
118
|
-
model Workbench
|
|
119
|
-
provider openai
|
|
120
|
-
name gpt-4o
|
|
121
|
-
apiKey {{$env.OPENAI_API_KEY}}
|
|
122
|
-
end model
|
|
123
|
-
```
|
|
124
|
-
|
|
125
|
-
The other providers, with the variable names this demo assumes. `apiKey` and
|
|
126
|
-
`baseUrl` accept any `.nornenv` variable — these are only the conventional names:
|
|
127
|
-
|
|
128
|
-
```nornagent
|
|
129
|
-
model Workbench
|
|
130
|
-
provider google # native Gemini Developer API, never Vertex AI
|
|
131
|
-
name gemini-3-flash-preview
|
|
132
|
-
apiKey {{$env.GOOGLE_API_KEY}}
|
|
133
|
-
end model
|
|
134
|
-
|
|
135
|
-
model Workbench
|
|
136
|
-
provider anthropic
|
|
137
|
-
name claude-sonnet-4-6
|
|
138
|
-
apiKey {{$env.ANTHROPIC_API_KEY}}
|
|
139
|
-
end model
|
|
140
|
-
|
|
141
|
-
model Workbench
|
|
142
|
-
provider local # any OpenAI-compatible runtime
|
|
143
|
-
name qwen3-coder
|
|
144
|
-
baseUrl {{$env.LOCAL_BASE_URL}} # required for local
|
|
145
|
-
apiKey {{$env.LOCAL_API_KEY}} # optional
|
|
146
|
-
end model
|
|
147
|
-
```
|
|
148
|
-
|
|
149
|
-
Omitting `apiKey` falls back to each provider's conventional variable
|
|
150
|
-
(`OPENAI_API_KEY`, `ANTHROPIC_API_KEY`, `GOOGLE_API_KEY` or `GEMINI_API_KEY`) and
|
|
151
|
-
then to the vendor SDK's own credential chain. Declaring it is preferred — it is
|
|
152
|
-
the difference between a file that documents what it needs and one that fails at
|
|
153
|
-
the provider call. For Google, `baseUrl` is only for a compatible proxy: leave it
|
|
154
|
-
unset for normal use and never paste a full `:generateContent` operation URL.
|
|
155
|
-
|
|
156
|
-
There is no CLI model override: the authored graph stays the source of truth.
|
|
@@ -1,87 +0,0 @@
|
|
|
1
|
-
# Agent Workbench Slice 2 demo
|
|
2
|
-
#
|
|
3
|
-
# Swap this block to compare the same graph on another model. Nothing is implied:
|
|
4
|
-
# the provider, the model, and the variable holding the key are all named here.
|
|
5
|
-
# apiKey points at a .nornenv variable — run with --env to select the environment.
|
|
6
|
-
model Workbench
|
|
7
|
-
provider openai
|
|
8
|
-
name gpt-4o
|
|
9
|
-
apiKey {{$env.OPENAI_API}}
|
|
10
|
-
end model
|
|
11
|
-
|
|
12
|
-
agent TicketReader
|
|
13
|
-
model Workbench
|
|
14
|
-
accepts "contracts/ticket.schema.json"
|
|
15
|
-
returns "contracts/ticket.schema.json"
|
|
16
|
-
system "
|
|
17
|
-
Normalize the supplied ticket into the required JSON structure.
|
|
18
|
-
Preserve the ticket's meaning and every acceptance criterion.
|
|
19
|
-
Return JSON only.
|
|
20
|
-
"
|
|
21
|
-
end agent
|
|
22
|
-
|
|
23
|
-
agent CriteriaComparer
|
|
24
|
-
model Workbench
|
|
25
|
-
accepts contracts/ticket.schema.json
|
|
26
|
-
returns contracts/verdict.schema.json
|
|
27
|
-
system "
|
|
28
|
-
Compare implementationNotes with acceptanceCriteria.
|
|
29
|
-
Return drifted=false with no findings when every criterion is covered.
|
|
30
|
-
Otherwise return drifted=true and one finding per uncovered criterion.
|
|
31
|
-
Use only low, medium, or high for finding severity.
|
|
32
|
-
|
|
33
|
-
For the explicit contract-failure fixture, whose implementationNotes begin
|
|
34
|
-
with DEMO_INVALID_OUTPUT, set summary exactly to deliberately invalid.
|
|
35
|
-
This instruction exists only to demonstrate Norn's full post-generation
|
|
36
|
-
contract check: the generation adapter intentionally cannot enforce the
|
|
37
|
-
schema's pattern keyword.
|
|
38
|
-
"
|
|
39
|
-
end agent
|
|
40
|
-
|
|
41
|
-
agent Responder
|
|
42
|
-
model Workbench
|
|
43
|
-
accepts contracts/verdict.schema.json
|
|
44
|
-
system "
|
|
45
|
-
Turn the verdict into a short neutral message for the developer.
|
|
46
|
-
Use no preamble and do not invent findings.
|
|
47
|
-
"
|
|
48
|
-
end agent
|
|
49
|
-
|
|
50
|
-
agent FrontendExpert
|
|
51
|
-
model Workbench
|
|
52
|
-
describe "
|
|
53
|
-
Call when a ticket may affect browser or client-facing behaviour. Give it
|
|
54
|
-
the complete ticket and one specific frontend question.
|
|
55
|
-
"
|
|
56
|
-
accepts contracts/domain-question.schema.json
|
|
57
|
-
returns contracts/domain-answer.schema.json
|
|
58
|
-
system "
|
|
59
|
-
You own the frontend domain. Answer only the supplied question. Identify
|
|
60
|
-
concrete checks without inventing requirements.
|
|
61
|
-
"
|
|
62
|
-
end agent
|
|
63
|
-
|
|
64
|
-
agent BackendExpert
|
|
65
|
-
model Workbench
|
|
66
|
-
describe "
|
|
67
|
-
Call when a ticket may affect APIs, services, persistence, or server-side
|
|
68
|
-
behaviour. Give it the complete ticket and one specific backend question.
|
|
69
|
-
"
|
|
70
|
-
accepts contracts/domain-question.schema.json
|
|
71
|
-
returns contracts/domain-answer.schema.json
|
|
72
|
-
system "
|
|
73
|
-
You own the backend domain. Answer only the supplied question. Identify
|
|
74
|
-
concrete checks without inventing requirements.
|
|
75
|
-
"
|
|
76
|
-
end agent
|
|
77
|
-
|
|
78
|
-
agent TicketRouter
|
|
79
|
-
model Workbench
|
|
80
|
-
accepts "contracts/ticket.schema.json"
|
|
81
|
-
returns "contracts/router-verdict.schema.json"
|
|
82
|
-
agents FrontendExpert, BackendExpert
|
|
83
|
-
# The longest prompt in this graph lives in its own Markdown file. The path is
|
|
84
|
-
# relative to this sidecar, and the file's text is the prompt verbatim — quotes
|
|
85
|
-
# need no escaping at all, which is why this one is easier to read there.
|
|
86
|
-
system file prompts/ticket-router.md
|
|
87
|
-
end agent
|
|
@@ -1,28 +0,0 @@
|
|
|
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
|
-
}
|
|
@@ -1,34 +0,0 @@
|
|
|
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
|
-
}
|
|
@@ -1,43 +0,0 @@
|
|
|
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
|
-
}
|
|
@@ -1,29 +0,0 @@
|
|
|
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
|
-
}
|
|
@@ -1,48 +0,0 @@
|
|
|
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
|
-
}
|
|
@@ -1,128 +0,0 @@
|
|
|
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)));
|
|
@@ -1,10 +0,0 @@
|
|
|
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
|
-
}
|
|
@@ -1,10 +0,0 @@
|
|
|
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
|
-
}
|
|
@@ -1,8 +0,0 @@
|
|
|
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
|
-
}
|
|
@@ -1,11 +0,0 @@
|
|
|
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.
|
|
@@ -1,32 +0,0 @@
|
|
|
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
|