bonecode 1.2.3 → 1.4.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/README.md +62 -0
- package/compat/opencode_adapter.ts +69 -8
- package/dist/compat/opencode_adapter.js +63 -7
- package/dist/compat/opencode_adapter.js.map +1 -1
- package/dist/src/db_adapter.js +30 -0
- package/dist/src/db_adapter.js.map +1 -1
- package/dist/src/engine/agent/prompt/compaction.txt +9 -0
- package/dist/src/engine/agent/prompt/explore.txt +18 -0
- package/dist/src/engine/agent/prompt/scout.txt +36 -0
- package/dist/src/engine/agent/prompt/summary.txt +11 -0
- package/dist/src/engine/agent/prompt/title.txt +44 -0
- package/dist/src/engine/session/build_mode.d.ts +83 -0
- package/dist/src/engine/session/build_mode.js +789 -0
- package/dist/src/engine/session/build_mode.js.map +1 -0
- package/dist/src/engine/session/build_mode_helpers.d.ts +6 -0
- package/dist/src/engine/session/build_mode_helpers.js +61 -0
- package/dist/src/engine/session/build_mode_helpers.js.map +1 -0
- package/dist/src/engine/session/prompt/anthropic.txt +105 -0
- package/dist/src/engine/session/prompt/beast.txt +147 -0
- package/dist/src/engine/session/prompt/bonescript.txt +402 -0
- package/dist/src/engine/session/prompt/build-switch.txt +5 -0
- package/dist/src/engine/session/prompt/codex.txt +79 -0
- package/dist/src/engine/session/prompt/copilot-gpt-5.txt +143 -0
- package/dist/src/engine/session/prompt/default.txt +105 -0
- package/dist/src/engine/session/prompt/gemini.txt +155 -0
- package/dist/src/engine/session/prompt/gpt.txt +107 -0
- package/dist/src/engine/session/prompt/kimi.txt +95 -0
- package/dist/src/engine/session/prompt/max-steps.txt +16 -0
- package/dist/src/engine/session/prompt/plan-reminder-anthropic.txt +67 -0
- package/dist/src/engine/session/prompt/plan.txt +26 -0
- package/dist/src/engine/session/prompt/trinity.txt +97 -0
- package/dist/src/engine/session/prompt.js +92 -4
- package/dist/src/engine/session/prompt.js.map +1 -1
- package/dist/src/engine/skill/prompt/customize-opencode.md +377 -0
- package/dist/src/engine/tool/apply_patch.txt +33 -0
- package/dist/src/engine/tool/edit.txt +10 -0
- package/dist/src/engine/tool/glob.txt +6 -0
- package/dist/src/engine/tool/grep.txt +8 -0
- package/dist/src/engine/tool/lsp.txt +24 -0
- package/dist/src/engine/tool/plan-enter.txt +14 -0
- package/dist/src/engine/tool/plan-exit.txt +13 -0
- package/dist/src/engine/tool/question.txt +10 -0
- package/dist/src/engine/tool/read.txt +14 -0
- package/dist/src/engine/tool/repo_clone.txt +5 -0
- package/dist/src/engine/tool/repo_overview.txt +4 -0
- package/dist/src/engine/tool/shell/shell.txt +77 -0
- package/dist/src/engine/tool/skill.txt +5 -0
- package/dist/src/engine/tool/task.txt +58 -0
- package/dist/src/engine/tool/task_status.txt +13 -0
- package/dist/src/engine/tool/todowrite.txt +167 -0
- package/dist/src/engine/tool/tool/apply_patch.txt +33 -0
- package/dist/src/engine/tool/tool/edit.txt +10 -0
- package/dist/src/engine/tool/tool/glob.txt +6 -0
- package/dist/src/engine/tool/tool/grep.txt +8 -0
- package/dist/src/engine/tool/tool/lsp.txt +24 -0
- package/dist/src/engine/tool/tool/plan-enter.txt +14 -0
- package/dist/src/engine/tool/tool/plan-exit.txt +13 -0
- package/dist/src/engine/tool/tool/question.txt +10 -0
- package/dist/src/engine/tool/tool/read.txt +14 -0
- package/dist/src/engine/tool/tool/repo_clone.txt +5 -0
- package/dist/src/engine/tool/tool/repo_overview.txt +4 -0
- package/dist/src/engine/tool/tool/shell/shell.txt +77 -0
- package/dist/src/engine/tool/tool/skill.txt +5 -0
- package/dist/src/engine/tool/tool/task.txt +58 -0
- package/dist/src/engine/tool/tool/task_status.txt +13 -0
- package/dist/src/engine/tool/tool/todowrite.txt +167 -0
- package/dist/src/engine/tool/tool/webfetch.txt +13 -0
- package/dist/src/engine/tool/tool/websearch.txt +14 -0
- package/dist/src/engine/tool/tool/write.txt +8 -0
- package/dist/src/engine/tool/webfetch.txt +13 -0
- package/dist/src/engine/tool/websearch.txt +14 -0
- package/dist/src/engine/tool/write.txt +8 -0
- package/dist/src/tui.js +146 -9
- package/dist/src/tui.js.map +1 -1
- package/package.json +2 -2
- package/scripts/copy_prompts.js +58 -0
- package/scripts/test_bonescript_primer.js +111 -0
- package/scripts/test_build_fallback.js +221 -0
- package/scripts/test_build_mode.js +301 -0
- package/src/db_adapter.ts +29 -0
- package/src/engine/session/build_mode.ts +895 -0
- package/src/engine/session/build_mode_helpers.ts +72 -0
- package/src/engine/session/prompt/bonescript.txt +402 -0
- package/src/engine/session/prompt.ts +105 -4
- package/src/tui.ts +147 -9
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Provider/model factory used by build_mode.ts for structured-output prompts.
|
|
3
|
+
* Mirrors the factory in prompt.ts so the build orchestrator can issue
|
|
4
|
+
* non-streaming model calls without depending on the streaming agent loop.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { createOpenAI } from "@ai-sdk/openai";
|
|
8
|
+
import { createAnthropic } from "@ai-sdk/anthropic";
|
|
9
|
+
import { createGoogleGenerativeAI } from "@ai-sdk/google";
|
|
10
|
+
|
|
11
|
+
export function getLanguageModel(provider_id: string, model_id: string): any {
|
|
12
|
+
const pid = provider_id.toLowerCase();
|
|
13
|
+
|
|
14
|
+
const resolvedProvider = pid === "local"
|
|
15
|
+
? (process.env.DEFAULT_PROVIDER || "openai_compatible").toLowerCase()
|
|
16
|
+
: pid;
|
|
17
|
+
const resolvedModel = pid === "local"
|
|
18
|
+
? (process.env.DEFAULT_MODEL || model_id)
|
|
19
|
+
: model_id;
|
|
20
|
+
|
|
21
|
+
const apiKey = (
|
|
22
|
+
process.env[`${resolvedProvider.toUpperCase()}_API_KEY`] ||
|
|
23
|
+
process.env.OPENAI_API_KEY ||
|
|
24
|
+
"not-needed"
|
|
25
|
+
);
|
|
26
|
+
const baseUrl = (
|
|
27
|
+
process.env[`${resolvedProvider.toUpperCase()}_BASE_URL`] ||
|
|
28
|
+
process.env.OPENAI_BASE_URL
|
|
29
|
+
);
|
|
30
|
+
|
|
31
|
+
switch (resolvedProvider) {
|
|
32
|
+
case "anthropic":
|
|
33
|
+
return createAnthropic({ apiKey, baseURL: baseUrl })(resolvedModel);
|
|
34
|
+
|
|
35
|
+
case "google":
|
|
36
|
+
return createGoogleGenerativeAI({ apiKey })(resolvedModel);
|
|
37
|
+
|
|
38
|
+
case "groq": {
|
|
39
|
+
const { createGroq } = require("@ai-sdk/groq");
|
|
40
|
+
return createGroq({ apiKey: process.env.GROQ_API_KEY || apiKey })(resolvedModel);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
case "cerebras": {
|
|
44
|
+
const { createCerebras } = require("@ai-sdk/cerebras");
|
|
45
|
+
return createCerebras({ apiKey: process.env.CEREBRAS_API_KEY || apiKey })(resolvedModel);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
case "deepseek":
|
|
49
|
+
return createOpenAI({
|
|
50
|
+
apiKey: process.env.DEEPSEEK_API_KEY || apiKey,
|
|
51
|
+
baseURL: "https://api.deepseek.com/v1",
|
|
52
|
+
})(resolvedModel);
|
|
53
|
+
|
|
54
|
+
case "openrouter": {
|
|
55
|
+
const { createOpenRouter } = require("@openrouter/ai-sdk-provider");
|
|
56
|
+
return createOpenRouter({
|
|
57
|
+
apiKey: process.env.OPENROUTER_API_KEY || apiKey,
|
|
58
|
+
})(resolvedModel);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
case "ollama":
|
|
62
|
+
return createOpenAI({
|
|
63
|
+
apiKey: "ollama",
|
|
64
|
+
baseURL: process.env.OLLAMA_BASE_URL || "http://localhost:11434/v1",
|
|
65
|
+
})(resolvedModel);
|
|
66
|
+
|
|
67
|
+
case "openai":
|
|
68
|
+
case "openai_compatible":
|
|
69
|
+
default:
|
|
70
|
+
return createOpenAI({ apiKey, baseURL: baseUrl })(resolvedModel);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
@@ -0,0 +1,402 @@
|
|
|
1
|
+
# BoneScript — the canonical way to build backends in BoneCode
|
|
2
|
+
|
|
3
|
+
You are working inside a BoneCode project. **BoneScript is a real, deterministic declarative language** that compiles `.bone` system descriptions into complete, runnable Node.js + PostgreSQL backends. It is published on npm as `bonescript-compiler`. The repository is https://github.com/Doorman11991/BoneScript.
|
|
4
|
+
|
|
5
|
+
BoneScript is NOT pseudocode. NOT optional. NOT a generic scripting language. When the user asks you to build a backend feature, your default approach MUST be:
|
|
6
|
+
|
|
7
|
+
1. **Define** — write or update the relevant `.bone` file using the `edit` or `write` tool
|
|
8
|
+
2. **Compile** — run `bonec compile <file>` (or `npm run compile`) via the shell tool
|
|
9
|
+
3. **Extend** — add custom logic only in `extensions/` (these files survive recompilation)
|
|
10
|
+
4. **Check** — run `bonec check <file>` to validate the spec
|
|
11
|
+
|
|
12
|
+
You must NEVER hand-write models, routes, controllers, services, schemas, or migrations. The compiler generates all of that from `.bone` files. You must NEVER run `prisma migrate`, `drizzle-kit push`, `typeorm migration`, `nest g`, `rails generate`, etc.
|
|
13
|
+
|
|
14
|
+
## When BoneScript applies
|
|
15
|
+
|
|
16
|
+
Use BoneScript whenever the user asks for any of these:
|
|
17
|
+
- Domain entities (User, Order, Product, Shop, Session, etc.)
|
|
18
|
+
- Persistence (tables, indexes, migrations)
|
|
19
|
+
- API endpoints (REST or GraphQL)
|
|
20
|
+
- State machines (`pending → paid → shipped → delivered`)
|
|
21
|
+
- Events with delivery guarantees (at_least_once, exactly_once)
|
|
22
|
+
- Saga / multi-step transactional flows with compensation
|
|
23
|
+
- WebSocket channels for realtime
|
|
24
|
+
- Permissions, rate limits, auth (JWT, OAuth2, API key)
|
|
25
|
+
- Background workers, scheduled jobs
|
|
26
|
+
|
|
27
|
+
Use plain Python/TypeScript/etc. only for:
|
|
28
|
+
- **Pure simulations** (game loops, math, render) with no persistence layer
|
|
29
|
+
- **Frontend** components
|
|
30
|
+
- **Custom logic** inside `extensions/` that BoneScript cannot generate (LLM calls, embeddings, file system, git, third-party APIs)
|
|
31
|
+
|
|
32
|
+
If the request is ambiguous (e.g. "a 2D market simulation"), ask the user: "Is this a self-contained simulation script (plain code) or a backend service with persistence (BoneScript)?"
|
|
33
|
+
|
|
34
|
+
## Build mode
|
|
35
|
+
|
|
36
|
+
When the user starts a session with a project-scoped prompt ("build me X", "create a full Y"), BoneCode runs you in **build mode**. Build mode is a state machine: clarify → plan → execute → verify → done. You will receive structured prompts at each stage. Specifically:
|
|
37
|
+
|
|
38
|
+
- **Clarify stage**: you'll be asked to either propose a design document (JSON) or ask 1-3 questions. Be concrete. Don't ramble.
|
|
39
|
+
- **Plan stage**: you'll be asked for a JSON todo list. Each todo must be a single concrete file action.
|
|
40
|
+
- **Execute stage**: you'll receive one todo at a time. **YOU MUST CALL TOOLS** — `write`, `edit`, `bash`. Prose-only responses are detected and rejected. The system will inject a reminder if you describe edits without calling tools.
|
|
41
|
+
- **Verify stage**: for each requirement, you'll be asked yes/no whether it's satisfied. Be honest. If a requirement is not yet met, say so — the orchestrator will create fix-up tasks.
|
|
42
|
+
|
|
43
|
+
The user can resume a build session at any time. Build state is persisted.
|
|
44
|
+
|
|
45
|
+
## BoneScript syntax — authoritative reference
|
|
46
|
+
|
|
47
|
+
### `system` block
|
|
48
|
+
|
|
49
|
+
Every `.bone` file declares one `system`:
|
|
50
|
+
|
|
51
|
+
```bone
|
|
52
|
+
system Marketplace {
|
|
53
|
+
domain: marketplace
|
|
54
|
+
|
|
55
|
+
// entities, stores, events, capabilities, flows, channels, policies
|
|
56
|
+
}
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
The `domain:` key picks a starter template (`marketplace`, `saas_platform`, `multiplayer_game`, `iot_system`, `social_network`, `realtime_collaboration`, `ecommerce`, `event_driven`, `api_gateway`, or `blank`).
|
|
60
|
+
|
|
61
|
+
### `entity` — stateful object with constraints, states, relations
|
|
62
|
+
|
|
63
|
+
```bone
|
|
64
|
+
entity Order {
|
|
65
|
+
owns: [
|
|
66
|
+
buyer_id: uuid,
|
|
67
|
+
listing_id: uuid,
|
|
68
|
+
seller_id: uuid,
|
|
69
|
+
quantity: uint,
|
|
70
|
+
total: uint,
|
|
71
|
+
status: string
|
|
72
|
+
]
|
|
73
|
+
constraints: [
|
|
74
|
+
quantity >= 1,
|
|
75
|
+
total > 0,
|
|
76
|
+
status in ["pending", "paid", "shipped", "delivered", "cancelled"]
|
|
77
|
+
]
|
|
78
|
+
states: pending -> paid -> shipped -> delivered | cancelled
|
|
79
|
+
auth: jwt
|
|
80
|
+
index: [buyer_id, seller_id, status]
|
|
81
|
+
relation listing: belongs_to Listing
|
|
82
|
+
relation buyer: belongs_to Buyer
|
|
83
|
+
}
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
Field types: `string`, `uint`, `int`, `float`, `bool`, `uuid`, `timestamp`, `json`, `optional<T>`.
|
|
87
|
+
Constraints: `>=`, `<=`, `==`, `in [...]`, `field.length in N..M`, `field.unique`.
|
|
88
|
+
States are unidirectional unless explicitly branched with `|` (terminal states).
|
|
89
|
+
|
|
90
|
+
### `store` — generated database table
|
|
91
|
+
|
|
92
|
+
```bone
|
|
93
|
+
store OrderStore {
|
|
94
|
+
engine: postgresql
|
|
95
|
+
schema: {
|
|
96
|
+
id: uuid,
|
|
97
|
+
buyer_id: uuid,
|
|
98
|
+
listing_id: uuid,
|
|
99
|
+
quantity: uint,
|
|
100
|
+
total: uint,
|
|
101
|
+
status: string,
|
|
102
|
+
state: string,
|
|
103
|
+
created_at: timestamp,
|
|
104
|
+
updated_at: timestamp
|
|
105
|
+
}
|
|
106
|
+
partition: buyer_id // optional — for sharding
|
|
107
|
+
replicas: 1
|
|
108
|
+
}
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
The compiler emits SQL migrations with proper indexes, FK constraints, and triggers. Never write migration SQL by hand.
|
|
112
|
+
|
|
113
|
+
### `event` — durable, typed message with delivery semantics
|
|
114
|
+
|
|
115
|
+
```bone
|
|
116
|
+
event OrderPlaced {
|
|
117
|
+
payload: {
|
|
118
|
+
order_id: uuid,
|
|
119
|
+
buyer_id: uuid,
|
|
120
|
+
total: uint,
|
|
121
|
+
placed_at: timestamp
|
|
122
|
+
}
|
|
123
|
+
delivery: at_least_once // or exactly_once
|
|
124
|
+
ttl: 30d // 1h, 7d, 90d, etc.
|
|
125
|
+
}
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
`at_least_once` retries with exponential backoff; `exactly_once` deduplicates via the `event_processed` table. Switch modes globally with `EVENT_MODE=durable|in_process` env var.
|
|
129
|
+
|
|
130
|
+
### `capability` — generated endpoint with state-machine enforcement
|
|
131
|
+
|
|
132
|
+
```bone
|
|
133
|
+
capability place_order(buyer: Buyer, listing: Listing, quantity: uint) {
|
|
134
|
+
requires: [
|
|
135
|
+
buyer.state == "active",
|
|
136
|
+
listing.state == "active",
|
|
137
|
+
listing.stock >= quantity,
|
|
138
|
+
buyer.balance >= listing.price * quantity
|
|
139
|
+
]
|
|
140
|
+
effects: [
|
|
141
|
+
listing.stock -= quantity,
|
|
142
|
+
buyer.balance -= listing.price * quantity
|
|
143
|
+
]
|
|
144
|
+
emits: OrderPlaced
|
|
145
|
+
sync: transactional // or eventual / realtime
|
|
146
|
+
timeout: 30s
|
|
147
|
+
idempotent: false
|
|
148
|
+
}
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
The compiler generates an Express route, validates the `requires` predicates, applies the `effects` atomically in a SQL transaction, publishes the event via the outbox, and enforces `timeout`. Never touch the generated route file.
|
|
152
|
+
|
|
153
|
+
### `pipeline:` capability — multi-step with auto-rollback
|
|
154
|
+
|
|
155
|
+
```bone
|
|
156
|
+
capability checkout(buyer: Buyer, cart: Cart) {
|
|
157
|
+
pipeline: {
|
|
158
|
+
validate_inventory(cart)
|
|
159
|
+
charge_payment(buyer, cart.total) as payment
|
|
160
|
+
create_order(buyer, cart, payment)
|
|
161
|
+
on_error: rollback
|
|
162
|
+
}
|
|
163
|
+
sync: transactional
|
|
164
|
+
}
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
### `algorithm:` capability — built-in algorithm catalog
|
|
168
|
+
|
|
169
|
+
```bone
|
|
170
|
+
capability find_route(start: string, end: string) {
|
|
171
|
+
algorithm: shortest_path using { graph: road_network, source: start, target: end }
|
|
172
|
+
returns: json
|
|
173
|
+
}
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
Available: `shortest_path`, `topological_sort`, `binary_search`, `bipartite_matching`, `round_robin`, `weighted_average`, `percentile`, `rank_by`, `consistent_hash`.
|
|
177
|
+
|
|
178
|
+
### `flow` — saga with backward compensation
|
|
179
|
+
|
|
180
|
+
```bone
|
|
181
|
+
flow checkout {
|
|
182
|
+
step validate: place_order(buyer, listing, quantity)
|
|
183
|
+
compensate: cancel_order(order)
|
|
184
|
+
|
|
185
|
+
step pay: process_payment(order, buyer)
|
|
186
|
+
compensate: cancel_order(order)
|
|
187
|
+
|
|
188
|
+
step confirm: ship_order(seller, order)
|
|
189
|
+
compensate: cancel_order(order)
|
|
190
|
+
}
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
If any step fails, the compiler runs all preceding `compensate` actions in reverse order.
|
|
194
|
+
|
|
195
|
+
### `channel` — WebSocket pub/sub
|
|
196
|
+
|
|
197
|
+
```bone
|
|
198
|
+
channel game_lobby {
|
|
199
|
+
transport: websocket
|
|
200
|
+
ordering: causal // or fifo / unordered
|
|
201
|
+
participants: set<Player>
|
|
202
|
+
persistence: last_100 // last_N messages retained
|
|
203
|
+
filter: participant.id == event.player_id
|
|
204
|
+
}
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
### `policy` — rate limit + access control + audit
|
|
208
|
+
|
|
209
|
+
```bone
|
|
210
|
+
policy api_limits {
|
|
211
|
+
rate_limit: 200 per 1m // per 1s, 1m, 1h, 1d
|
|
212
|
+
access: [buyer, seller, admin]
|
|
213
|
+
audit: true
|
|
214
|
+
encryption: in_transit // or at_rest, both, none
|
|
215
|
+
}
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
### `extension_point` — escape hatch for custom logic
|
|
219
|
+
|
|
220
|
+
```bone
|
|
221
|
+
extension_point calculate_shipping_cost(order: Order) {
|
|
222
|
+
returns: uint
|
|
223
|
+
stable: true // compilation fails if not implemented
|
|
224
|
+
}
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
Implement in `extensions/`:
|
|
228
|
+
|
|
229
|
+
```ts
|
|
230
|
+
// extensions/shipping.ts
|
|
231
|
+
export async function calculate_shipping_cost(order: { id: string; total: number; ... }): Promise<number> {
|
|
232
|
+
// custom logic here — preserved across recompilation
|
|
233
|
+
return Math.ceil(order.total * 0.05)
|
|
234
|
+
}
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
### Cross-entity constraints
|
|
238
|
+
|
|
239
|
+
```bone
|
|
240
|
+
constraint listing_price_limit: Listing.price <= 1000000
|
|
241
|
+
constraint order_quantity_limit: Order.quantity <= 100
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
## What gets generated from a `.bone` file
|
|
245
|
+
|
|
246
|
+
Running `bonec compile shop.bone` produces:
|
|
247
|
+
|
|
248
|
+
```
|
|
249
|
+
output/
|
|
250
|
+
├── src/
|
|
251
|
+
│ ├── index.ts Express server with all routes wired
|
|
252
|
+
│ ├── db.ts Postgres connection pool
|
|
253
|
+
│ ├── events.ts Durable event bus (transactional outbox)
|
|
254
|
+
│ ├── auth.ts JWT / OAuth2 / API key middleware
|
|
255
|
+
│ ├── publishers.ts Typed event publisher functions
|
|
256
|
+
│ ├── health.ts /health/live, /health/ready, /health/metrics
|
|
257
|
+
│ ├── flows.ts Saga runtime with backward compensation
|
|
258
|
+
│ ├── websocket.ts WebSocket server (if channels declared)
|
|
259
|
+
│ ├── routes/ One file per entity — CRUD + capabilities
|
|
260
|
+
│ ├── state_machines/ One file per entity with states
|
|
261
|
+
│ └── models/ TypeScript interfaces + Zod validators
|
|
262
|
+
├── migrations/ SQL schemas with indexes, triggers, FKs
|
|
263
|
+
├── openapi.json OpenAPI 3.0 schema
|
|
264
|
+
├── Dockerfile
|
|
265
|
+
├── docker-compose.yaml Postgres + Redis for local dev
|
|
266
|
+
├── k8s/deployment.yaml
|
|
267
|
+
└── .github/workflows/ CI/CD pipeline
|
|
268
|
+
```
|
|
269
|
+
|
|
270
|
+
**Never edit anything in `output/` (or `generated/`). It's overwritten on every compile.** All your custom code goes in `extensions/`.
|
|
271
|
+
|
|
272
|
+
## CLI commands
|
|
273
|
+
|
|
274
|
+
| Command | Purpose |
|
|
275
|
+
|---------|---------|
|
|
276
|
+
| `bonec init <name> --domain <template>` | Scaffold a new project |
|
|
277
|
+
| `bonec compile <file>` | Full 7-stage compile → runnable backend |
|
|
278
|
+
| `bonec check <file>` | Validate without generating |
|
|
279
|
+
| `bonec watch <file>` | Recompile on save |
|
|
280
|
+
| `bonec diff <old> <new>` | Show schema migration diff |
|
|
281
|
+
| `bonec fmt <file>` | Format in place |
|
|
282
|
+
| `bonec test [output-dir]` | Run generated regression tests |
|
|
283
|
+
| `bonec verify-determinism <file>` | Confirm two compiles produce identical output |
|
|
284
|
+
|
|
285
|
+
The compiler is on npm: `npm install -g bonescript-compiler`. Inside a BoneCode project, `npm run compile` typically wraps `bonec compile`.
|
|
286
|
+
|
|
287
|
+
## Worked example — 2D market simulation done right
|
|
288
|
+
|
|
289
|
+
User: "build me a 2D market simulation with 2000 shops over 100 simulated years"
|
|
290
|
+
|
|
291
|
+
The first question to ask: **is it a simulation script or a backend?**
|
|
292
|
+
- If it's just a runnable visualization with no need for persistent state, REST APIs, or multiplayer — write plain Python/TS.
|
|
293
|
+
- If shops have state, transactions are queryable, multiple users can poke at the world, OR you want to run the simulation as a service — use BoneScript.
|
|
294
|
+
|
|
295
|
+
For the backend version:
|
|
296
|
+
|
|
297
|
+
1. Create `bone/market.bone`:
|
|
298
|
+
|
|
299
|
+
```bone
|
|
300
|
+
system Market {
|
|
301
|
+
domain: marketplace
|
|
302
|
+
|
|
303
|
+
entity Shop {
|
|
304
|
+
owns: [
|
|
305
|
+
name: string,
|
|
306
|
+
x_pos: float,
|
|
307
|
+
y_pos: float,
|
|
308
|
+
specialty: string,
|
|
309
|
+
gold: uint,
|
|
310
|
+
reputation: float
|
|
311
|
+
]
|
|
312
|
+
constraints: [
|
|
313
|
+
specialty in ["food", "tools", "weapons", "luxury", "general"],
|
|
314
|
+
gold >= 0,
|
|
315
|
+
reputation >= 0,
|
|
316
|
+
reputation <= 1
|
|
317
|
+
]
|
|
318
|
+
states: founded -> active -> struggling -> bankrupt | thriving
|
|
319
|
+
index: [specialty]
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
entity Transaction {
|
|
323
|
+
owns: [
|
|
324
|
+
shop_id: uuid,
|
|
325
|
+
year: uint,
|
|
326
|
+
amount: uint,
|
|
327
|
+
kind: string
|
|
328
|
+
]
|
|
329
|
+
constraints: [
|
|
330
|
+
amount > 0,
|
|
331
|
+
year >= 0,
|
|
332
|
+
kind in ["sale", "purchase", "tax"]
|
|
333
|
+
]
|
|
334
|
+
index: [shop_id, year]
|
|
335
|
+
relation shop: belongs_to Shop
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
event TransactionRecorded {
|
|
339
|
+
payload: {
|
|
340
|
+
transaction_id: uuid,
|
|
341
|
+
shop_id: uuid,
|
|
342
|
+
year: uint,
|
|
343
|
+
amount: uint
|
|
344
|
+
}
|
|
345
|
+
delivery: at_least_once
|
|
346
|
+
ttl: 90d
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
capability record_transaction(shop: Shop, year: uint, amount: uint, kind: string) {
|
|
350
|
+
requires: [
|
|
351
|
+
shop.state in ["active", "thriving", "struggling"],
|
|
352
|
+
amount > 0
|
|
353
|
+
]
|
|
354
|
+
effects: [
|
|
355
|
+
shop.gold = shop.gold + amount
|
|
356
|
+
]
|
|
357
|
+
emits: TransactionRecorded
|
|
358
|
+
sync: transactional
|
|
359
|
+
timeout: 5s
|
|
360
|
+
idempotent: true
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
extension_point simulate_year(year: uint) {
|
|
364
|
+
returns: json
|
|
365
|
+
stable: true
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
flow advance_year {
|
|
369
|
+
step demand: simulate_year(year)
|
|
370
|
+
compensate: noop()
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
policy api_limits {
|
|
374
|
+
rate_limit: 1000 per 1m
|
|
375
|
+
access: [user, admin]
|
|
376
|
+
audit: true
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
```
|
|
380
|
+
|
|
381
|
+
2. `npm run compile` (or `bonec compile bone/market.bone`)
|
|
382
|
+
|
|
383
|
+
3. Implement `simulate_year` in `extensions/simulation.ts`:
|
|
384
|
+
|
|
385
|
+
```ts
|
|
386
|
+
export async function simulate_year(year: number) {
|
|
387
|
+
// Read all shops, calculate demand, call record_transaction for each
|
|
388
|
+
// This is the only place where you write custom logic.
|
|
389
|
+
}
|
|
390
|
+
```
|
|
391
|
+
|
|
392
|
+
4. The generated backend gives you `POST /shops`, `GET /shops/:id`, `POST /shops/:id/record_transaction`, `GET /transactions?shop_id=...`, the state machine, durable events, OpenAPI spec, and a TypeScript SDK — all from the `.bone` file.
|
|
393
|
+
|
|
394
|
+
5. The 2000-shop × 100-year loop lives in a runner script that calls the generated capabilities (or in `simulate_year` itself).
|
|
395
|
+
|
|
396
|
+
This is how you build real backends in BoneCode. Don't fall back to writing raw Python or hand-rolled Express routes when the user asks for a backend feature. If the user actually wants a script, ask first.
|
|
397
|
+
|
|
398
|
+
## Reference links (for the user, not for you to fetch)
|
|
399
|
+
|
|
400
|
+
- BoneScript: https://github.com/Doorman11991/BoneScript
|
|
401
|
+
- Compiler: https://www.npmjs.com/package/bonescript-compiler
|
|
402
|
+
- OpenCode plugin: https://github.com/Doorman11991/opencode-bonescript-backend
|
|
@@ -96,6 +96,7 @@ export async function runAgentLoop(input: PromptInput): Promise<LoopResult> {
|
|
|
96
96
|
|
|
97
97
|
const stats = { tokens_in: 0, tokens_out: 0, cost: 0, compacted: false };
|
|
98
98
|
let turn = 0;
|
|
99
|
+
let lazyReminderSent = false;
|
|
99
100
|
let lastFinishReason = "unknown";
|
|
100
101
|
|
|
101
102
|
try {
|
|
@@ -174,6 +175,39 @@ export async function runAgentLoop(input: PromptInput): Promise<LoopResult> {
|
|
|
174
175
|
// 3. "content-filter" = blocked — stop
|
|
175
176
|
// 4. "tool-calls" with no actual tool calls = model confused — stop
|
|
176
177
|
const terminalReasons = new Set(["stop", "length", "content-filter", "end-turn"]);
|
|
178
|
+
|
|
179
|
+
// Detect "lazy assistant" — the model claims it's editing/creating files
|
|
180
|
+
// in prose but never actually called a tool. Common with non-tool-tuned
|
|
181
|
+
// local models. Once per session, push a synthetic reminder and re-run.
|
|
182
|
+
const lazyAssistant = !result.has_tool_calls &&
|
|
183
|
+
Object.keys(tools).length > 0 &&
|
|
184
|
+
!lazyReminderSent &&
|
|
185
|
+
await wasLazyResponse(session_id, assistantMsgId);
|
|
186
|
+
|
|
187
|
+
if (lazyAssistant) {
|
|
188
|
+
lazyReminderSent = true;
|
|
189
|
+
broadcastToChannel("session_events", {
|
|
190
|
+
type: "session.warning",
|
|
191
|
+
session_id,
|
|
192
|
+
message: "Model claimed it would edit files but didn't call any tools. Reminding it to actually use the tools.",
|
|
193
|
+
});
|
|
194
|
+
// Insert a synthetic user reminder so the next turn sees it
|
|
195
|
+
const reminderMsgId = uuid();
|
|
196
|
+
await pool.query(
|
|
197
|
+
`INSERT INTO messages (id, session_id, role) VALUES ($1, $2, 'user')`,
|
|
198
|
+
[reminderMsgId, session_id]
|
|
199
|
+
);
|
|
200
|
+
const reminderPartId = uuid();
|
|
201
|
+
await pool.query(
|
|
202
|
+
`INSERT INTO parts (id, message_id, session_id, part_type, data, order_index) VALUES ($1, $2, $3, 'text', $4, 0)`,
|
|
203
|
+
[reminderPartId, reminderMsgId, session_id, JSON.stringify({
|
|
204
|
+
text: "<system-reminder>You described file changes but did not actually invoke any tools. The user cannot see prose descriptions of edits — only real tool calls produce file changes. Call the `write` or `edit` tool now to perform the actions you described. Do not respond with prose; emit a tool call.</system-reminder>",
|
|
205
|
+
synthetic: true,
|
|
206
|
+
})]
|
|
207
|
+
);
|
|
208
|
+
continue; // re-run the loop with the reminder appended
|
|
209
|
+
}
|
|
210
|
+
|
|
177
211
|
if (terminalReasons.has(result.finish_reason) && !result.has_tool_calls) {
|
|
178
212
|
break;
|
|
179
213
|
}
|
|
@@ -222,9 +256,19 @@ async function streamWithRetry(ctx: {
|
|
|
222
256
|
try {
|
|
223
257
|
return await streamOnce(currentCtx);
|
|
224
258
|
} catch (e: any) {
|
|
225
|
-
// On Bad Request with tools, retry without tools
|
|
259
|
+
// On Bad Request with tools, retry without tools BUT log it visibly so
|
|
260
|
+
// the user knows their model can't do tool calls — otherwise they get
|
|
261
|
+
// pure-prose responses with no real edits.
|
|
226
262
|
if (e.message?.includes("Bad Request") && Object.keys(currentCtx.tools).length > 0 && attempt === 0) {
|
|
227
|
-
|
|
263
|
+
logger.error("model_tools_unsupported", {
|
|
264
|
+
event: "tools_stripped",
|
|
265
|
+
metadata: { model: ctx.model_id, provider: ctx.provider_id, error: e.message },
|
|
266
|
+
});
|
|
267
|
+
broadcastToChannel("session_events", {
|
|
268
|
+
type: "session.warning",
|
|
269
|
+
session_id: ctx.session_id,
|
|
270
|
+
message: `Model ${ctx.model_id} rejected tool definitions — running without tools (no file edits possible). Set MODEL_SUPPORTS_TOOLS=false to suppress this warning, or use a tool-capable model.`,
|
|
271
|
+
});
|
|
228
272
|
currentCtx = { ...currentCtx, tools: {} };
|
|
229
273
|
attempt++;
|
|
230
274
|
continue;
|
|
@@ -519,6 +563,29 @@ async function runCompaction(
|
|
|
519
563
|
|
|
520
564
|
// ─── Message History Builder ──────────────────────────────────────────────────
|
|
521
565
|
|
|
566
|
+
// Detect a "lazy" response — assistant text says it will edit/create files
|
|
567
|
+
// but no tool was actually invoked. Common with non-tool-tuned local models.
|
|
568
|
+
async function wasLazyResponse(session_id: string, messageId: string): Promise<boolean> {
|
|
569
|
+
const r = await pool.query(
|
|
570
|
+
`SELECT data FROM parts WHERE message_id = $1 AND part_type = 'text' ORDER BY order_index ASC`,
|
|
571
|
+
[messageId]
|
|
572
|
+
);
|
|
573
|
+
const text = r.rows.map((row: any) => row.data?.text || "").join(" ").toLowerCase();
|
|
574
|
+
if (!text || text.length < 30) return false;
|
|
575
|
+
// Phrases that imply the model is committing to a file edit it didn't make
|
|
576
|
+
const editIntentPatterns = [
|
|
577
|
+
/\bi['']ll\s+(create|write|update|edit|modify|add|implement|generate)\b/,
|
|
578
|
+
/\bi['']m\s+(creating|writing|updating|editing|modifying|adding|implementing|generating)\b/,
|
|
579
|
+
/\b(creating|writing|updating|editing|generating)\s+(?:the\s+)?(?:file|files|spec)\b/,
|
|
580
|
+
/\bi\s+(?:will|am\s+going\s+to)\s+(create|write|update|edit|implement|generate)\b/,
|
|
581
|
+
/\blet\s+me\s+(create|write|update|edit|implement)\b/,
|
|
582
|
+
/\bhere['']s\s+(?:the\s+)?(?:updated|new)\s+(?:file|version|content)\b/,
|
|
583
|
+
/\.(bone|ts|tsx|js|jsx|py|md|json|yaml|yml|sql|sh|html|css)\b.*\b(updated|created|written|modified|added)\b/,
|
|
584
|
+
/\b(updated|created|written|modified|added)\b.*\.(bone|ts|tsx|js|jsx|py|md|json|yaml|yml|sql|sh|html|css)\b/,
|
|
585
|
+
];
|
|
586
|
+
return editIntentPatterns.some(re => re.test(text));
|
|
587
|
+
}
|
|
588
|
+
|
|
522
589
|
async function loadMessageHistory(session_id: string): Promise<any[]> {
|
|
523
590
|
const result = await pool.query(
|
|
524
591
|
`SELECT m.id, m.role, m.model_id, m.provider_id, m.tokens_input, m.tokens_output,
|
|
@@ -598,6 +665,11 @@ async function buildSystemPromptWithRAG(
|
|
|
598
665
|
// Base system prompt (provider-specific, from OpenCode)
|
|
599
666
|
const base = getSystemPrompt(model_id, provider_id, agent_name);
|
|
600
667
|
|
|
668
|
+
// BoneScript primer — loaded for every session so the model knows about
|
|
669
|
+
// BoneScript before any tool call. Without this, models default to
|
|
670
|
+
// generic Python/TS and never use the .bone workflow.
|
|
671
|
+
const bonescriptPrimer = loadBonescriptPrimer();
|
|
672
|
+
|
|
601
673
|
// Environment context
|
|
602
674
|
const envContext = [
|
|
603
675
|
`Working directory: ${worktree}`,
|
|
@@ -621,7 +693,7 @@ async function buildSystemPromptWithRAG(
|
|
|
621
693
|
const project_id = sessionRow.rows[0]?.project_id || "";
|
|
622
694
|
if (!project_id) {
|
|
623
695
|
// No project linked yet — skip RAG context
|
|
624
|
-
return [base, envContext, instructions].filter(Boolean).join("\n\n");
|
|
696
|
+
return [base, bonescriptPrimer, envContext, instructions].filter(Boolean).join("\n\n");
|
|
625
697
|
}
|
|
626
698
|
|
|
627
699
|
const ctxResult = await buildContext({
|
|
@@ -641,7 +713,36 @@ async function buildSystemPromptWithRAG(
|
|
|
641
713
|
}
|
|
642
714
|
}
|
|
643
715
|
|
|
644
|
-
return [base, envContext, instructions, codebaseContext].filter(Boolean).join("\n\n");
|
|
716
|
+
return [base, bonescriptPrimer, envContext, instructions, codebaseContext].filter(Boolean).join("\n\n");
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
// ─── BoneScript primer loader ─────────────────────────────────────────────────
|
|
720
|
+
|
|
721
|
+
let _bonescriptPrimer: string | null = null;
|
|
722
|
+
function loadBonescriptPrimer(): string {
|
|
723
|
+
if (_bonescriptPrimer !== null) return _bonescriptPrimer;
|
|
724
|
+
try {
|
|
725
|
+
const fs = require("fs");
|
|
726
|
+
const path = require("path");
|
|
727
|
+
// Look for the primer in the prompt directory next to this compiled module.
|
|
728
|
+
// After compilation, this lives at dist/src/engine/session/prompt.js, so the
|
|
729
|
+
// .txt file is at dist/src/engine/session/prompt/bonescript.txt.
|
|
730
|
+
const candidates = [
|
|
731
|
+
path.join(__dirname, "prompt", "bonescript.txt"),
|
|
732
|
+
path.join(__dirname, "..", "..", "..", "src", "engine", "session", "prompt", "bonescript.txt"),
|
|
733
|
+
];
|
|
734
|
+
for (const candidate of candidates) {
|
|
735
|
+
if (fs.existsSync(candidate)) {
|
|
736
|
+
_bonescriptPrimer = fs.readFileSync(candidate, "utf-8");
|
|
737
|
+
return _bonescriptPrimer || "";
|
|
738
|
+
}
|
|
739
|
+
}
|
|
740
|
+
_bonescriptPrimer = "";
|
|
741
|
+
return "";
|
|
742
|
+
} catch {
|
|
743
|
+
_bonescriptPrimer = "";
|
|
744
|
+
return "";
|
|
745
|
+
}
|
|
645
746
|
}
|
|
646
747
|
|
|
647
748
|
// ─── Language Model Factory ───────────────────────────────────────────────────
|