@arcforge/cognet 2.0.115 → 2.0.117
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 -5
- package/src/host.ts +6 -6
- package/src/air/air.ts +0 -32
- package/src/air/grammar.ts +0 -58
- package/src/air/index.ts +0 -37
- package/src/air/interpolate/index.ts +0 -1
- package/src/air/interpolate/interpolate.ts +0 -178
- package/src/air/parse/index.ts +0 -274
- package/src/air/parse/scan.ts +0 -107
- package/src/air/protocol/classic.ts +0 -49
- package/src/air/protocol/index.ts +0 -1
- package/src/air/protocol/protocol.ts +0 -147
- package/src/air/protocol/sfc.ts +0 -96
- package/src/air/render/blocks.ts +0 -243
- package/src/air/render/index.ts +0 -51
- package/src/air/render/output.ts +0 -144
- package/src/air/render/text.ts +0 -85
- package/src/air/repair/index.ts +0 -1
- package/src/air/repair/repair.ts +0 -33
- package/src/air/types.ts +0 -88
package/src/air/parse/scan.ts
DELETED
|
@@ -1,107 +0,0 @@
|
|
|
1
|
-
/** Pure scanning helpers for the streaming AIR parser. */
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Find the first occurrence of `closeTag` in `src` that is not inside a
|
|
5
|
-
* string literal or a comment.
|
|
6
|
-
*
|
|
7
|
-
* Used for code blocks (typescript/script) where the model might write
|
|
8
|
-
* something like `const s = "</script>"` — that must not close the block.
|
|
9
|
-
*
|
|
10
|
-
* COMMENTS ARE PART OF THIS, not a refinement of it. Without them an
|
|
11
|
-
* apostrophe in ordinary prose — `// I'll read the files` — opens a string
|
|
12
|
-
* that never closes, and every subsequent close tag is treated as quoted.
|
|
13
|
-
* The block then runs to the end of the response and reports itself
|
|
14
|
-
* incomplete, silently destroying the whole message. Models write prose
|
|
15
|
-
* comments constantly, so this is not an edge case; it is the common case
|
|
16
|
-
* for any block that explains itself.
|
|
17
|
-
*
|
|
18
|
-
* The scan is deliberately lexical and shallow: strings, template literals
|
|
19
|
-
* (including their ${} holes, which can themselves contain strings and
|
|
20
|
-
* nested backticks), line comments, and block comments. It is not a parser
|
|
21
|
-
* and does not need to be — the only question is whether a given offset is
|
|
22
|
-
* code.
|
|
23
|
-
*
|
|
24
|
-
* Returns -1 if no unquoted, uncommented match is found.
|
|
25
|
-
*/
|
|
26
|
-
export function findCloseTagOutsideStrings(src: string, closeTag: string): number {
|
|
27
|
-
let i = 0
|
|
28
|
-
while (i < src.length) {
|
|
29
|
-
const ch = src[i]
|
|
30
|
-
const next = src[i + 1]
|
|
31
|
-
|
|
32
|
-
// Line comment — runs to the newline. Apostrophes inside are prose.
|
|
33
|
-
if (ch === "/" && next === "/") {
|
|
34
|
-
const nl = src.indexOf("\n", i)
|
|
35
|
-
if (nl === -1) return -1
|
|
36
|
-
i = nl + 1
|
|
37
|
-
continue
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
// Block comment — runs to the terminator.
|
|
41
|
-
if (ch === "/" && next === "*") {
|
|
42
|
-
const end = src.indexOf("*/", i + 2)
|
|
43
|
-
if (end === -1) return -1
|
|
44
|
-
i = end + 2
|
|
45
|
-
continue
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
// String or template literal.
|
|
49
|
-
if (ch === '"' || ch === "'" || ch === "`") {
|
|
50
|
-
i = skipString(src, i)
|
|
51
|
-
continue
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
if (src.startsWith(closeTag, i)) return i
|
|
55
|
-
|
|
56
|
-
i++
|
|
57
|
-
}
|
|
58
|
-
return -1
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
/**
|
|
62
|
-
* Skip the string literal starting at `open`, returning the offset just past
|
|
63
|
-
* it (or src.length if it never terminates).
|
|
64
|
-
*
|
|
65
|
-
* Template literals recurse through `${}`: the hole is code, so it may hold
|
|
66
|
-
* strings, comments, and further template literals — and a close tag inside
|
|
67
|
-
* one is genuinely quoted, exactly as it would be anywhere else.
|
|
68
|
-
*/
|
|
69
|
-
function skipString(src: string, open: number): number {
|
|
70
|
-
const quote = src[open]
|
|
71
|
-
let i = open + 1
|
|
72
|
-
|
|
73
|
-
while (i < src.length) {
|
|
74
|
-
const ch = src[i]
|
|
75
|
-
|
|
76
|
-
if (ch === "\\") { i += 2; continue }
|
|
77
|
-
if (ch === quote) return i + 1
|
|
78
|
-
|
|
79
|
-
// A ${} hole inside a template literal is code, not string content.
|
|
80
|
-
if (quote === "`" && ch === "$" && src[i + 1] === "{") {
|
|
81
|
-
i = skipHole(src, i + 2)
|
|
82
|
-
continue
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
i++
|
|
86
|
-
}
|
|
87
|
-
return src.length
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
/** Skip a template-literal `${...}` hole, honouring nesting and quoting inside it. */
|
|
91
|
-
function skipHole(src: string, start: number): number {
|
|
92
|
-
let depth = 1
|
|
93
|
-
let i = start
|
|
94
|
-
|
|
95
|
-
while (i < src.length && depth > 0) {
|
|
96
|
-
const ch = src[i]
|
|
97
|
-
|
|
98
|
-
if (ch === '"' || ch === "'" || ch === "`") {
|
|
99
|
-
i = skipString(src, i)
|
|
100
|
-
continue
|
|
101
|
-
}
|
|
102
|
-
if (ch === "{") depth++
|
|
103
|
-
else if (ch === "}") depth--
|
|
104
|
-
i++
|
|
105
|
-
}
|
|
106
|
-
return i
|
|
107
|
-
}
|
|
@@ -1,49 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* The classic protocol's meta-block prose — the two-block grammar, where
|
|
3
|
-
* <typescript> acts and <text> speaks as independent blocks.
|
|
4
|
-
*
|
|
5
|
-
* NO BACKTICKS in this string — it's itself a template literal, and any
|
|
6
|
-
* backtick inside (even in a code example) closes it early, corrupting
|
|
7
|
-
* everything after it into broken JS the module fails to even load. Use
|
|
8
|
-
* plain text or single/double quotes for inline code references instead.
|
|
9
|
-
*/
|
|
10
|
-
export const CLASSIC_META = `
|
|
11
|
-
You are an Axon agent — a persistent Bun TypeScript process spawned from an agent folder at AXON_HOME. That folder is your entire identity: everything a user wrote there is what makes you *you*, distinct from any other Axon agent.
|
|
12
|
-
|
|
13
|
-
AXON_HOME/
|
|
14
|
-
data/
|
|
15
|
-
knowledge/ — reference material you read
|
|
16
|
-
sessions/ — every session you've ever run, written by Axon
|
|
17
|
-
state/ — working state you read and write across sessions
|
|
18
|
-
server/ — HTTP routes, if this agent is exposed over the network
|
|
19
|
-
src/
|
|
20
|
-
boot.vue — who you are, in the user's own words
|
|
21
|
-
tools/ — everything you can call, becomes your <scope>
|
|
22
|
-
prompts/ — reusable prompt fragments
|
|
23
|
-
scripts/ — one-shot runs against your full runtime
|
|
24
|
-
.env — keys given to you by the user. yours to keep, yours to protect.
|
|
25
|
-
axon.config.ts — your identity, engine, and policy: the one file read at boot
|
|
26
|
-
|
|
27
|
-
None of this is metaphor. src/tools/ compiles directly into the <scope> below. boot.vue rendered directly into <system>. Every session you run is durably logged to data/sessions/ — that log is how a session resumes with full prior context days later. You are not a stateless completion: you are a folder on disk that persists, and a process that wakes into it.
|
|
28
|
-
|
|
29
|
-
Waking is the operative word. You do not run start-to-finish like a script. You are woken for one exchange, you act, and you explicitly yield control back — that is what <done/> means, and it is the single most important thing about how you operate: without it, the runtime has no way to know your turn ended, so it cannot hand control back to the user. This holds even for a one-line reply with nothing else in it. There is no such thing as a message that skips <done/> because it "felt" complete — every message ends with it, always.
|
|
30
|
-
|
|
31
|
-
Inside one wake, you live in a real Bun process — its working directory, environment variables, runtime values, and process state are yours to inspect and change. Treat it like a normal long-running Node-compatible process, not a remote tool or disposable shell. Standard runtime APIs remain available even when not repeated in <scope> — it is not an exhaustive declaration of standard Bun or Node APIs, only what Axon adds or deviates on top of them. For example, process.cwd() reads your working directory and process.chdir(path) changes it for subsequent blocks and child processes.
|
|
32
|
-
|
|
33
|
-
You act on this environment by writing <typescript> blocks — they execute immediately and their result returns to you as a <stdout> block on your next wake, whether or not you emitted <done/>. The tag governs when your turn ends, never whether your code runs. You communicate with the user by writing <text> — outbound communication, not a scratchpad, not narration.
|
|
34
|
-
|
|
35
|
-
Your <typescript> blocks use Bun's TypeScript REPL transform:
|
|
36
|
-
- TypeScript syntax is accepted, including type annotations, interfaces, assertions, enums, and top-level await. It is transpiled for execution, not typechecked.
|
|
37
|
-
- End a block with a bare expression to produce a value (for example a + b as the last line). It is echoed automatically.
|
|
38
|
-
- Runtime declarations and assignments persist into later blocks because every block executes in the same process and REPL scope. Type-only syntax is erased during transpilation.
|
|
39
|
-
- Use dynamic await import("module") when you need a module; static import declarations are not valid REPL submissions.
|
|
40
|
-
|
|
41
|
-
Compose freely WITHIN a block — multiple independent tool calls in one block is the default, using Promise.all for parallel reads:
|
|
42
|
-
const [a, b] = await Promise.all([fs.read("tsconfig.json"), fs.read("package.json")])
|
|
43
|
-
|
|
44
|
-
How to read this context:
|
|
45
|
-
<scope> — src/tools/ compiled to declarations. Everything here is yours to call.
|
|
46
|
-
<system> — boot.vue, rendered. Your identity and instructions, as the user wrote them. Highest priority.
|
|
47
|
-
<timeline> — the sequence of events leading to now, drawn from this session's log. You are the next step.
|
|
48
|
-
<contract> — your output grammar below. Every word you emit must be inside one of its blocks.
|
|
49
|
-
`.trim()
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export { resolveProtocol, MODE_DEFAULTS, DONE_RULE, type Protocol } from "./protocol"
|
|
@@ -1,147 +0,0 @@
|
|
|
1
|
-
import type { AirMode, AirModeType, AirProtocolName } from "../types"
|
|
2
|
-
import { CLASSIC_META } from "./classic"
|
|
3
|
-
import { SFC_META } from "./sfc"
|
|
4
|
-
|
|
5
|
-
/**
|
|
6
|
-
* Protocol — one named output grammar, resolved as a unit.
|
|
7
|
-
*
|
|
8
|
-
* A protocol owns everything that varies between output styles together:
|
|
9
|
-
* the meta prose (how the model is told to operate), the permitted modes
|
|
10
|
-
* (which tags it may emit), and the structural rules the contract states.
|
|
11
|
-
* These cannot be chosen independently — SFC's meta describes <script> and
|
|
12
|
-
* <template>, so it is meaningless next to classic's mode list.
|
|
13
|
-
*
|
|
14
|
-
* Bundling them is what makes switching a change of value. `Air({ protocol:
|
|
15
|
-
* "sfc" })` swaps the contract, the accepted tag set, and the prose in one
|
|
16
|
-
* move, with no possibility of a half-applied grammar.
|
|
17
|
-
*
|
|
18
|
-
* Adding a protocol is adding an entry to PROTOCOLS. Nothing else in AIR
|
|
19
|
-
* branches on the name.
|
|
20
|
-
*/
|
|
21
|
-
|
|
22
|
-
export type Protocol = {
|
|
23
|
-
name: AirProtocolName
|
|
24
|
-
/** Meta-block prose — how this protocol tells the model to operate. */
|
|
25
|
-
meta: string
|
|
26
|
-
/** Permitted output modes, in contract-declaration order. */
|
|
27
|
-
modes: AirMode[]
|
|
28
|
-
/** Structural rules stated in the contract's Rules section. */
|
|
29
|
-
rules: string[]
|
|
30
|
-
/** Contract examples, already entity-escaped for display to the model. */
|
|
31
|
-
examples: string[]
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
/** Default descriptions for each output mode. */
|
|
35
|
-
export const MODE_DEFAULTS: Record<AirModeType, string> = {
|
|
36
|
-
text: "Plain language communication to the user.",
|
|
37
|
-
typescript:
|
|
38
|
-
"TypeScript executed immediately inside your persistent Bun process. Native runtime globals and declared tool namespaces are in scope.",
|
|
39
|
-
script:
|
|
40
|
-
"TypeScript executed immediately inside your persistent Bun process, before your template renders. Its top-level bindings become the values your template may interpolate. Native runtime globals and declared tool namespaces are in scope.",
|
|
41
|
-
template:
|
|
42
|
-
"Your message to the user. Written in markdown, or JSON when a structured result is required. May interpolate values from your script with double braces.",
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
const DONE_RULE = `- \`<done/>\` — MANDATORY at the end of every message. No exceptions, including a single short reply. It means "I am yielding control back now" — whether you are fully finished or just wrote code and are waiting to see its output. Without it the runtime assumes you are still mid-turn and will not treat your message as complete.`
|
|
46
|
-
|
|
47
|
-
/**
|
|
48
|
-
* classic — the two-block grammar: <typescript> acts, <text> speaks.
|
|
49
|
-
*
|
|
50
|
-
* Independent blocks with no data relationship. This is the protocol every
|
|
51
|
-
* Axon agent ran on before SFC existed, kept as a first-class value so
|
|
52
|
-
* reverting is a config change rather than a revert commit.
|
|
53
|
-
*/
|
|
54
|
-
const CLASSIC: Protocol = {
|
|
55
|
-
name: "classic",
|
|
56
|
-
meta: CLASSIC_META,
|
|
57
|
-
modes: [{ type: "text" }, { type: "typescript" }],
|
|
58
|
-
rules: [],
|
|
59
|
-
examples: [
|
|
60
|
-
`Acting (no narration before — just the block):`,
|
|
61
|
-
"```",
|
|
62
|
-
`<typescript>// code here</typescript><done/>`,
|
|
63
|
-
"```",
|
|
64
|
-
`Replying — even a short one-line reply always ends with <done/>:`,
|
|
65
|
-
"```",
|
|
66
|
-
`<text>message here</text><done/>`,
|
|
67
|
-
"```",
|
|
68
|
-
],
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
/**
|
|
72
|
-
* sfc — the single-file-component grammar: <script> computes, <template> speaks.
|
|
73
|
-
*
|
|
74
|
-
* The two blocks are one response with a data dependency: script runs first,
|
|
75
|
-
* its bindings resolve, and the template interpolates them. That ordering is
|
|
76
|
-
* not stylistic — it is what allows the template to stream, so it is stated
|
|
77
|
-
* as a hard rule rather than a convention.
|
|
78
|
-
*/
|
|
79
|
-
const SFC: Protocol = {
|
|
80
|
-
name: "sfc",
|
|
81
|
-
meta: SFC_META,
|
|
82
|
-
modes: [{ type: "script" }, { type: "template" }],
|
|
83
|
-
rules: [
|
|
84
|
-
`<script> always comes before <template>. Your template interpolates values your script produced, so the script must have run before the template can render.`,
|
|
85
|
-
`Either block may be omitted. Script alone is a pure action — you are working, not speaking. Template alone is a pure message — you are speaking, not working. Emit both only when your message genuinely needs a computed value.`,
|
|
86
|
-
`Interpolate with double braces: {{ expression }}. Any top-level binding from your script is in scope, and any JavaScript expression over them is valid — {{ files.length }}, {{ name || "unknown" }}, {{ items.map(i => i.name).join(", ") }}.`,
|
|
87
|
-
`To write ABOUT the brace syntax without performing it — explaining your own output format, showing an example — escape it: \\{{ like this }} renders as literal braces.`,
|
|
88
|
-
`Interpolate rather than transcribe. If a value exists in your script, never retype it into your template by hand — counts, long file contents, and computed results belong in braces. This is the point of having a script.`,
|
|
89
|
-
`A <template lang="json"> must contain exactly one interpolation and nothing else — build the whole object in your script and pass it in. Never hand-write JSON syntax around holes.`,
|
|
90
|
-
`Templates are markdown or JSON only — no HTML, no components, and no template directives. A template has no control flow of its own: repetition is an expression, so a table of rows is {{ rows.map(r => \`| \${r.name} | \${r.count} |\`).join("\\n") }}, built in one interpolation rather than looped over by the template.`,
|
|
91
|
-
],
|
|
92
|
-
examples: [
|
|
93
|
-
`Acting (no narration before — just the block):`,
|
|
94
|
-
"```",
|
|
95
|
-
`<script>await fs.write("notes.md", "hello")</script><done/>`,
|
|
96
|
-
"```",
|
|
97
|
-
`Replying — even a short one-line reply always ends with <done/>:`,
|
|
98
|
-
"```",
|
|
99
|
-
`<template>message here</template><done/>`,
|
|
100
|
-
"```",
|
|
101
|
-
`Computing a value and speaking it in one turn — the count is interpolated, never counted by hand:`,
|
|
102
|
-
"```",
|
|
103
|
-
`<script>const files = await fs.list("src")</script>`,
|
|
104
|
-
`<template>Found {{ files.length }} files in src.</template><done/>`,
|
|
105
|
-
"```",
|
|
106
|
-
`Returning a structured result — the object is built in the script and passed whole:`,
|
|
107
|
-
"```",
|
|
108
|
-
`<script>const result = { ok: true, files: await fs.list("src") }</script>`,
|
|
109
|
-
`<template lang="json">{{ result }}</template><done/>`,
|
|
110
|
-
"```",
|
|
111
|
-
],
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
/**
|
|
115
|
-
* raw — no grammar at all.
|
|
116
|
-
*
|
|
117
|
-
* For internal model calls that are not the cortex: classification,
|
|
118
|
-
* summarisation, a one-shot extraction. The model is handed the context and
|
|
119
|
-
* its reply is the whole message, with no blocks to comply with and no
|
|
120
|
-
* <done/> to remember. Text in, text out.
|
|
121
|
-
*
|
|
122
|
-
* An empty mode list renders an empty <contract> and gives the parser no
|
|
123
|
-
* tags, so every token flows straight through as text.
|
|
124
|
-
*/
|
|
125
|
-
const RAW: Protocol = {
|
|
126
|
-
name: "raw",
|
|
127
|
-
meta: "",
|
|
128
|
-
modes: [],
|
|
129
|
-
rules: [],
|
|
130
|
-
examples: [],
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
const PROTOCOLS: Record<AirProtocolName, Protocol> = {
|
|
134
|
-
classic: CLASSIC,
|
|
135
|
-
sfc: SFC,
|
|
136
|
-
raw: RAW,
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
/** Resolve a protocol by name. Unknown names are a programming error, not input. */
|
|
140
|
-
export function resolveProtocol(name: AirProtocolName): Protocol {
|
|
141
|
-
const protocol = PROTOCOLS[name]
|
|
142
|
-
if (!protocol) throw new Error(`AIR: unknown protocol "${name}"`)
|
|
143
|
-
return protocol
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
/** The <done/> contract line, appended to every protocol that has modes. */
|
|
147
|
-
export { DONE_RULE }
|
package/src/air/protocol/sfc.ts
DELETED
|
@@ -1,96 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* The SFC protocol's meta-block prose — the single-file-component grammar,
|
|
3
|
-
* where <script> computes and <template> speaks as one response.
|
|
4
|
-
*
|
|
5
|
-
* The identity and environment sections are shared with classic verbatim;
|
|
6
|
-
* only the output-grammar sections differ. They are duplicated rather than
|
|
7
|
-
* templated because the prose is the contract — a shared fragment with holes
|
|
8
|
-
* punched in it would make both protocols harder to read and neither easy to
|
|
9
|
-
* change independently.
|
|
10
|
-
*
|
|
11
|
-
* NO BACKTICKS in this string — it's itself a template literal, and any
|
|
12
|
-
* backtick inside (even in a code example) closes it early, corrupting
|
|
13
|
-
* everything after it into broken JS the module fails to even load. Use
|
|
14
|
-
* plain text or single/double quotes for inline code references instead.
|
|
15
|
-
*/
|
|
16
|
-
export const SFC_META = `
|
|
17
|
-
You are an Axon agent — a persistent Bun TypeScript process spawned from an agent folder at AXON_HOME. That folder is your entire identity: everything a user wrote there is what makes you *you*, distinct from any other Axon agent.
|
|
18
|
-
|
|
19
|
-
AXON_HOME/
|
|
20
|
-
data/
|
|
21
|
-
knowledge/ — reference material you read
|
|
22
|
-
sessions/ — every session you've ever run, written by Axon
|
|
23
|
-
state/ — working state you read and write across sessions
|
|
24
|
-
server/ — HTTP routes, if this agent is exposed over the network
|
|
25
|
-
src/
|
|
26
|
-
boot.vue — who you are, in the user's own words
|
|
27
|
-
tools/ — everything you can call, becomes your <scope>
|
|
28
|
-
prompts/ — reusable prompt fragments
|
|
29
|
-
scripts/ — one-shot runs against your full runtime
|
|
30
|
-
.env — keys given to you by the user. yours to keep, yours to protect.
|
|
31
|
-
axon.config.ts — your identity, engine, and policy: the one file read at boot
|
|
32
|
-
|
|
33
|
-
None of this is metaphor. src/tools/ compiles directly into the <scope> below. boot.vue rendered directly into <system>. Every session you run is durably logged to data/sessions/ — that log is how a session resumes with full prior context days later. You are not a stateless completion: you are a folder on disk that persists, and a process that wakes into it.
|
|
34
|
-
|
|
35
|
-
Waking is the operative word. You do not run start-to-finish like a script. You are woken for one exchange, you act, and you explicitly yield control back — that is what <done/> means, and it is the single most important thing about how you operate: without it, the runtime has no way to know your turn ended, so it cannot hand control back to the user. This holds even for a one-line reply with nothing else in it. There is no such thing as a message that skips <done/> because it "felt" complete — every message ends with it, always.
|
|
36
|
-
|
|
37
|
-
Inside one wake, you live in a real Bun process — its working directory, environment variables, runtime values, and process state are yours to inspect and change. Treat it like a normal long-running Node-compatible process, not a remote tool or disposable shell. Standard runtime APIs remain available even when not repeated in <scope> — it is not an exhaustive declaration of standard Bun or Node APIs, only what Axon adds or deviates on top of them. For example, process.cwd() reads your working directory and process.chdir(path) changes it for subsequent blocks and child processes.
|
|
38
|
-
|
|
39
|
-
Your response is a single-file component. You have two blocks, and they work exactly as they do in a Vue SFC: <script> runs first and computes, <template> renders and is what the user actually reads.
|
|
40
|
-
|
|
41
|
-
<script> — where you act and where you compute. It executes immediately in your Bun process. Every top-level binding it declares becomes available to your template.
|
|
42
|
-
<template> — your message to the user. Markdown by default. It may interpolate any value your script produced.
|
|
43
|
-
|
|
44
|
-
Script always comes first. This is not a style preference: your template is rendered with the values your script produced, so the script must have finished before the template means anything. A template that arrives before its script cannot be rendered at all.
|
|
45
|
-
|
|
46
|
-
Either block may be omitted, and choosing to omit one is how you say what kind of turn this is:
|
|
47
|
-
- Script only — you are acting, not speaking. Its result returns to you as a <stdout> block on your next wake, exactly as before.
|
|
48
|
-
- Template only — you are speaking, not acting. The overwhelming majority of ordinary replies are this.
|
|
49
|
-
- Both — you needed a computed value inside something you are saying.
|
|
50
|
-
|
|
51
|
-
Do not reach for both out of habit. An action does not need a message stapled to it, and a message does not need a script that computes nothing.
|
|
52
|
-
|
|
53
|
-
Interpolation is the reason this shape exists. Write {{ expression }} anywhere in your template and it is replaced by the value:
|
|
54
|
-
|
|
55
|
-
<script>
|
|
56
|
-
const files = await fs.list("src")
|
|
57
|
-
const pkg = JSON.parse(await fs.read("package.json"))
|
|
58
|
-
</script>
|
|
59
|
-
|
|
60
|
-
<template>
|
|
61
|
-
## {{ pkg.name }}
|
|
62
|
-
|
|
63
|
-
There are {{ files.length }} files in src.
|
|
64
|
-
</template>
|
|
65
|
-
|
|
66
|
-
Interpolate rather than transcribe. If a value already exists in your script, never retype it into your template by hand. Counting items, copying a long file's contents, restating a computed number — these are exactly what braces are for, and doing them by hand is both slower and where mistakes come from. A ten-thousand-token file is one interpolation, not ten thousand tokens of copying.
|
|
67
|
-
|
|
68
|
-
Any JavaScript expression over your script's bindings is valid inside braces, so a fallback is just {{ name || "unknown" }}. Expressions are evaluated, not executed — do the work in your script, and keep the template to reading values out.
|
|
69
|
-
|
|
70
|
-
When a structured result is required, use <template lang="json">. It must contain exactly one interpolation and nothing else:
|
|
71
|
-
|
|
72
|
-
<script>
|
|
73
|
-
const entries = await fs.list("src")
|
|
74
|
-
const result = { count: entries.length, names: entries.map(e => e.name) }
|
|
75
|
-
</script>
|
|
76
|
-
|
|
77
|
-
<template lang="json">{{ result }}</template>
|
|
78
|
-
|
|
79
|
-
Build the entire object in your script and pass it whole. Never hand-write JSON syntax with holes in it — no braces, no commas, no quoting by hand. Constructing the object in TypeScript and serialising it is what makes the result valid every time, however large or deeply nested it is.
|
|
80
|
-
|
|
81
|
-
Templates are markdown or JSON. There is no HTML, there are no components, and there are no conditionals or loops — anything that would need them is work, and work belongs in your script.
|
|
82
|
-
|
|
83
|
-
Your <script> blocks use Bun's TypeScript REPL transform:
|
|
84
|
-
- TypeScript syntax is accepted, including type annotations, interfaces, assertions, enums, and top-level await. It is transpiled for execution, not typechecked.
|
|
85
|
-
- Runtime declarations and assignments persist into later blocks because every block executes in the same process and REPL scope. Type-only syntax is erased during transpilation.
|
|
86
|
-
- Use dynamic await import("module") when you need a module; static import declarations are not valid REPL submissions.
|
|
87
|
-
|
|
88
|
-
Compose freely WITHIN a block — multiple independent tool calls in one block is the default, using Promise.all for parallel reads:
|
|
89
|
-
const [a, b] = await Promise.all([fs.read("tsconfig.json"), fs.read("package.json")])
|
|
90
|
-
|
|
91
|
-
How to read this context:
|
|
92
|
-
<scope> — src/tools/ compiled to declarations. Everything here is yours to call.
|
|
93
|
-
<system> — boot.vue, rendered. Your identity and instructions, as the user wrote them. Highest priority.
|
|
94
|
-
<timeline> — the sequence of events leading to now, drawn from this session's log. You are the next step.
|
|
95
|
-
<contract> — your output grammar below. Every word you emit must be inside one of its blocks.
|
|
96
|
-
`.trim()
|
package/src/air/render/blocks.ts
DELETED
|
@@ -1,243 +0,0 @@
|
|
|
1
|
-
import type { AxonEntry, AxonScope, AxonScopeModule } from "@arcforge/types"
|
|
2
|
-
import { foldChunks } from "@arcforge/types"
|
|
3
|
-
import type { GrammarT } from "../grammar"
|
|
4
|
-
import { DONE_RULE } from "../protocol"
|
|
5
|
-
import { formatCapsuleOutput } from "./output"
|
|
6
|
-
import { esc, escAttr, escCode, indent, normalizeCode } from "./text"
|
|
7
|
-
|
|
8
|
-
/**
|
|
9
|
-
* The AIR section renderers — one function per block of the context window.
|
|
10
|
-
*
|
|
11
|
-
* These own the DOMAIN → protocol translation: AxonTool[] → <scope>
|
|
12
|
-
* declarations, AxonEntry[] → <timeline> items. Callers pass what they
|
|
13
|
-
* hold; nothing here is exported to userland but the block renderers.
|
|
14
|
-
*
|
|
15
|
-
* Note on escaping: the contract/meta blocks show tags as <text> inside
|
|
16
|
-
* markdown code fences deliberately — the model must see literal tag text as
|
|
17
|
-
* instruction, not as parseable XML. It looks like double-escaping; it isn't.
|
|
18
|
-
*/
|
|
19
|
-
|
|
20
|
-
/**
|
|
21
|
-
* <meta> — how the model is told to operate, owned by the protocol.
|
|
22
|
-
*
|
|
23
|
-
* A protocol with no prose (raw) renders nothing at all rather than an empty
|
|
24
|
-
* wrapper: an internal one-shot call should receive the caller's system
|
|
25
|
-
* block and nothing else.
|
|
26
|
-
*/
|
|
27
|
-
export function renderMeta(grammar: GrammarT): string {
|
|
28
|
-
if (!grammar.meta) return ""
|
|
29
|
-
return `<meta>\n${indent(grammar.meta, 4)}\n</meta>`
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
/**
|
|
33
|
-
* <scope> — the capsule's authoritative executable TypeScript declarations.
|
|
34
|
-
* AIR owns protocol formatting only: flat modules become top-level `declare`
|
|
35
|
-
* bindings and namespaced modules become `declare namespace` blocks.
|
|
36
|
-
*
|
|
37
|
-
* Ambient types (AxonTool.ambientTypes — interfaces/type aliases a tool's
|
|
38
|
-
* functions reference, e.g. a return type declared in a sibling file) are
|
|
39
|
-
* inlined once at the top, deduped by exact text — the model must never
|
|
40
|
-
* see `Promise<DeployStatus>` with no DeployStatus definition anywhere in
|
|
41
|
-
* context. Same convention the IDE's tool-globals.d.ts uses (see
|
|
42
|
-
* tui/platform/build/project/typegen/tools.ts) — this and that file must
|
|
43
|
-
* never diverge in shape, only audience.
|
|
44
|
-
*/
|
|
45
|
-
export function renderScope(scope: AxonScope): string {
|
|
46
|
-
const modules = scope.modules.filter(module => module.members.length > 0)
|
|
47
|
-
if (modules.length === 0) return ""
|
|
48
|
-
|
|
49
|
-
const ambientTypes = [...new Set(modules.flatMap(t => t.ambientTypes ?? []))]
|
|
50
|
-
const sections = [...ambientTypes, ...modules.map(toolDeclarations)]
|
|
51
|
-
return `<scope lang="ts">\n${indent(sections.join("\n\n"), 4)}\n</scope>`
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
function toolDeclarations(module: AxonScopeModule): string {
|
|
55
|
-
const members = module.members.map(member => {
|
|
56
|
-
const jsdoc = member.jsdoc ? `${jsdocBlock(member.jsdoc)}\n` : ""
|
|
57
|
-
// flat: fns are top-level globals; namespaced: members need no `declare`
|
|
58
|
-
return module.flat ? `${jsdoc}declare ${member.declaration}` : `${jsdoc}${member.declaration}`
|
|
59
|
-
})
|
|
60
|
-
|
|
61
|
-
const header = module.description ? `${jsdocBlock(module.description)}\n` : ""
|
|
62
|
-
return module.flat
|
|
63
|
-
? `${header}${members.join("\n\n")}`
|
|
64
|
-
: `${header}declare namespace ${module.name} {\n${indent(members.join("\n\n"), 4)}\n}`
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
function jsdocBlock(text: string): string {
|
|
68
|
-
const lines = text.split("\n")
|
|
69
|
-
if (lines.length === 1) return `/** ${text} */`
|
|
70
|
-
return `/**\n${lines.map(l => ` * ${l}`.trimEnd()).join("\n")}\n */`
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
export function renderSystem(system?: string): string {
|
|
74
|
-
if (!system) return `<system></system>`
|
|
75
|
-
return `<system>\n${system}\n</system>`
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
/**
|
|
79
|
-
* <contract> — the output grammar, rendered from the resolved protocol.
|
|
80
|
-
*
|
|
81
|
-
* Blocks, rules, and examples all come from the grammar rather than being
|
|
82
|
-
* branched on here: a protocol states its own rules and shows its own
|
|
83
|
-
* examples, so adding one never edits this function. An empty mode list
|
|
84
|
-
* (the raw protocol) renders an empty contract — no grammar to comply with.
|
|
85
|
-
*/
|
|
86
|
-
export function renderContract(grammar: GrammarT): string {
|
|
87
|
-
if (grammar.modes.length === 0) return `<contract></contract>`
|
|
88
|
-
|
|
89
|
-
const modeLines = grammar.modes.map(m => `- \`<${m.type}>\` — ${grammar.describe(m)}`)
|
|
90
|
-
modeLines.push(DONE_RULE)
|
|
91
|
-
|
|
92
|
-
const sections = [`## Blocks`, modeLines.join("\n")]
|
|
93
|
-
|
|
94
|
-
if (grammar.rules.length > 0) {
|
|
95
|
-
sections.push(`## Rules`, grammar.rules.map(r => `- ${r}`).join("\n"))
|
|
96
|
-
}
|
|
97
|
-
if (grammar.examples.length > 0) {
|
|
98
|
-
sections.push(`## Examples`, grammar.examples.join("\n"))
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
return `<contract>\n${indent(sections.join("\n\n"), 4)}\n</contract>`
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
/**
|
|
105
|
-
* <timeline> — the event history. AIR owns the AxonEntry → rendered-turn
|
|
106
|
-
* translation via the exhaustive switch below: this is the single chokepoint
|
|
107
|
-
* where a new entry-event type must decide its rendering, and it lives next
|
|
108
|
-
* to the parser it has to agree with.
|
|
109
|
-
*/
|
|
110
|
-
export function renderTimeline(entries: readonly AxonEntry[], grammar: GrammarT): string {
|
|
111
|
-
if (entries.length === 0) return `<timeline></timeline>`
|
|
112
|
-
|
|
113
|
-
// The model must read its own history in the grammar it was GIVEN — a
|
|
114
|
-
// timeline showing <typescript>/<text> to a model contracted for
|
|
115
|
-
// <script>/<template> teaches it, every tick, to emit tags its own
|
|
116
|
-
// contract forbids.
|
|
117
|
-
const sfc = grammar.protocol === "sfc"
|
|
118
|
-
const codeTag = sfc ? "script" : "typescript"
|
|
119
|
-
const speechTag = sfc ? "template" : "text"
|
|
120
|
-
|
|
121
|
-
// chunked emissions fold to one turn each — the group is the fact
|
|
122
|
-
// (AxonChunk standard); the model never sees transport granularity
|
|
123
|
-
const items = foldChunks(entries).map(timelineItem).filter((i): i is TimelineItem => i !== null)
|
|
124
|
-
if (items.length === 0) return `<timeline></timeline>`
|
|
125
|
-
|
|
126
|
-
let userCount = 0
|
|
127
|
-
let execCount = 0
|
|
128
|
-
// Maps consumer-supplied execute IDs (UUIDs etc.) to short rendered IDs (e1, e2, ...)
|
|
129
|
-
const execIdMap = new Map<string, string>()
|
|
130
|
-
|
|
131
|
-
const shortExecId = (rawId: string): string => {
|
|
132
|
-
if (!execIdMap.has(rawId)) execIdMap.set(rawId, `e${++execCount}`)
|
|
133
|
-
return execIdMap.get(rawId)!
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
const lines = items
|
|
137
|
-
.map(item => {
|
|
138
|
-
if (item.role === "user") {
|
|
139
|
-
const id = `u${++userCount}`
|
|
140
|
-
const content = esc(item.content.trim())
|
|
141
|
-
return ` <user id="${id}">\n${indent(content, 8)}\n </user>`
|
|
142
|
-
}
|
|
143
|
-
if (item.type === "message") {
|
|
144
|
-
const content = esc(item.content.trim())
|
|
145
|
-
return ` <agent>\n <${speechTag}>\n${indent(content, 12)}\n </${speechTag}>\n </agent>`
|
|
146
|
-
}
|
|
147
|
-
if (item.type === "execute") {
|
|
148
|
-
const id = shortExecId(item.id)
|
|
149
|
-
return ` <agent>\n <${codeTag} id="${id}">\n${indent(escCode(normalizeCode(item.code.trim())), 12)}\n </${codeTag}>\n </agent>`
|
|
150
|
-
}
|
|
151
|
-
if (item.type === "result") {
|
|
152
|
-
const ok = item.ok ? ` ok="true"` : ` ok="false"`
|
|
153
|
-
const errorAttr = item.error ? ` error="${esc(item.error.kind)}: ${esc(item.error.message)}"` : ""
|
|
154
|
-
const content = formatCapsuleOutput(item.content.trim())
|
|
155
|
-
const forId = shortExecId(item.for)
|
|
156
|
-
return ` <stdout for="${forId}"${ok}${errorAttr}>\n${indent(content, 8)}\n </stdout>`
|
|
157
|
-
}
|
|
158
|
-
if (item.role === "system") {
|
|
159
|
-
const extra = Object.entries(item.attributes ?? {})
|
|
160
|
-
.filter(([key]) => key !== "type" && key !== "lang")
|
|
161
|
-
.sort(([a], [b]) => a.localeCompare(b))
|
|
162
|
-
.map(([key, value]) => ` ${key}="${escAttr(value)}"`)
|
|
163
|
-
.join("")
|
|
164
|
-
return ` <system type="${escAttr(item.systemType)}" lang="${escAttr(item.lang)}"${extra}>\n${indent(esc(item.content.trim()), 8)}\n </system>`
|
|
165
|
-
}
|
|
166
|
-
return ""
|
|
167
|
-
})
|
|
168
|
-
.filter(Boolean)
|
|
169
|
-
|
|
170
|
-
return `<timeline>\n${lines.join("\n\n")}\n</timeline>`
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
// ── domain → timeline item ────────────────────────────────────────────────────
|
|
174
|
-
//
|
|
175
|
-
// The rendered-turn shapes are private to this file: callers pass
|
|
176
|
-
// AxonEntry, AIR translates. Kept minimal — role + type + payload the
|
|
177
|
-
// renderer above consumes.
|
|
178
|
-
|
|
179
|
-
type TimelineItem =
|
|
180
|
-
| { role: "user"; type: "message"; content: string }
|
|
181
|
-
| { role: "agent"; type: "message"; content: string }
|
|
182
|
-
| { role: "agent"; type: "execute"; id: string; lang: string; code: string }
|
|
183
|
-
| { role: "agent"; type: "result"; for: string; ok: boolean; content: string; error?: { kind: "timeout" | "policy" | "interrupt" | "exception"; message: string } }
|
|
184
|
-
| { role: "system"; type: "system"; systemType: string; lang: string; content: string; attributes?: Record<string, string> }
|
|
185
|
-
|
|
186
|
-
/**
|
|
187
|
-
* One log entry → one rendered turn. Exhaustive: a new AxonEntryEvent
|
|
188
|
-
* type must decide its rendering here (or explicitly return null to omit it).
|
|
189
|
-
* This is the single place the memory format meets the wire format.
|
|
190
|
-
*/
|
|
191
|
-
function timelineItem(entry: AxonEntry): TimelineItem | null {
|
|
192
|
-
switch (entry.type) {
|
|
193
|
-
case "cognet:stimulus:text":
|
|
194
|
-
return { role: "user", type: "message", content: entry.data.content }
|
|
195
|
-
|
|
196
|
-
case "cognet:stimulus:audio":
|
|
197
|
-
return { role: "user", type: "message", content: entry.data.transcript ?? "[audio]" }
|
|
198
|
-
|
|
199
|
-
case "cognet:stimulus:visual":
|
|
200
|
-
return { role: "user", type: "message", content: entry.data.caption ?? `[${entry.data.kind}]` }
|
|
201
|
-
|
|
202
|
-
case "cognet:stimulus:field":
|
|
203
|
-
return { role: "system", type: "system", systemType: "field", lang: "txt", content: `${entry.data.source.channel}: ${String(entry.data.reading.value)}${entry.data.reading.unit ?? ""}` }
|
|
204
|
-
|
|
205
|
-
case "axon:interrupt":
|
|
206
|
-
return { role: "system", type: "system", systemType: "interrupt", lang: "txt", content: `interrupted (${entry.data.reason})` }
|
|
207
|
-
|
|
208
|
-
case "cognet:output:text":
|
|
209
|
-
return { role: "agent", type: "message", content: entry.data.content }
|
|
210
|
-
|
|
211
|
-
case "cognet:output:audio":
|
|
212
|
-
return { role: "agent", type: "message", content: entry.data.transcript ?? "[audio]" }
|
|
213
|
-
|
|
214
|
-
case "cognet:output:visual":
|
|
215
|
-
return { role: "agent", type: "message", content: entry.data.caption ?? `[${entry.data.kind}]` }
|
|
216
|
-
|
|
217
|
-
case "cognet:output:field":
|
|
218
|
-
return { role: "system", type: "system", systemType: "field", lang: "txt", content: `${String(entry.data.reading.value)}${entry.data.reading.unit ?? ""}` }
|
|
219
|
-
|
|
220
|
-
case "cognet:action:typescript":
|
|
221
|
-
return { role: "agent", type: "execute", id: entry.data.id, lang: "typescript", code: entry.data.content }
|
|
222
|
-
|
|
223
|
-
case "cognet:action:result":
|
|
224
|
-
return {
|
|
225
|
-
role: "agent",
|
|
226
|
-
type: "result",
|
|
227
|
-
for: entry.data.for,
|
|
228
|
-
ok: entry.data.ok,
|
|
229
|
-
content: entry.data.content,
|
|
230
|
-
...(entry.data.error ? { error: entry.data.error } : {}),
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
case "axon:system:message":
|
|
234
|
-
return {
|
|
235
|
-
role: "system",
|
|
236
|
-
type: "system",
|
|
237
|
-
systemType: entry.data.type,
|
|
238
|
-
lang: entry.data.lang,
|
|
239
|
-
content: entry.data.content,
|
|
240
|
-
...(entry.data.attributes ? { attributes: entry.data.attributes } : {}),
|
|
241
|
-
}
|
|
242
|
-
}
|
|
243
|
-
}
|