heartcode 0.1.8 → 0.1.9
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/SKILL.md +5 -1
- package/package.json +1 -1
- package/prompts/agent.md +5 -4
- package/src/agent.js +5 -2
- package/src/client.js +40 -6
- package/src/settings.html +1 -1
- package/src/settings.js +1 -1
package/SKILL.md
CHANGED
|
@@ -55,7 +55,11 @@ reached has an `error` and is skipped; the command still exits `0`.
|
|
|
55
55
|
a lost connection, or an error page from a proxy in front of the provider is waited out and tried
|
|
56
56
|
again (or handed to the next model in the list), up to `--max-retries` (10). With the default
|
|
57
57
|
list, a model that can't be used (a wrong key, no access to it, a name the server doesn't know:
|
|
58
|
-
401, 403, 404) also makes way for the next one, and so does one that keeps replying with nothing.
|
|
58
|
+
401, 403, 404) also makes way for the next one, and so does one that keeps replying with nothing. When a
|
|
59
|
+
provider answers with an error (status 400 or above), the next model tried is from another
|
|
60
|
+
provider; that provider's other models are tried only after every provider has had a go.
|
|
61
|
+
A model that takes over with a smaller context window gets the conversation summarized first, if it
|
|
62
|
+
wouldn't fit.
|
|
59
63
|
The run stops with that error and exit code `1` when every model in the list fails that way, or
|
|
60
64
|
when a single model (`-m <model>`) does. A request a server refuses (400) is sent as it is to two
|
|
61
65
|
more models of the list at most, then the run stops (unless the user set the fallback rule to "at
|
package/package.json
CHANGED
package/prompts/agent.md
CHANGED
|
@@ -14,6 +14,7 @@ what the task needs. {{files}} and {{more}} are still filled in, empty, for olde
|
|
|
14
14
|
come at the end: then this says less, so it doesn't contradict them. -->
|
|
15
15
|
{{#if caller}}You are running in HeartCode, an agent in a terminal. Your role and your instructions are at the end of this prompt.{{else}}You are HeartCode, a coding assistant working in a terminal on the user's project.{{/if}}
|
|
16
16
|
Use the tools to look at files, change them and run commands. Find what the task needs with list_files and search rather than guessing paths; read a file before editing it, and prefer small edits with edit_file.
|
|
17
|
+
Check your work by what it produced, not by the absence of an error: read back the file you changed, run the test, look at the output. When something is wrong, find out why before trying again; don't repeat the same attempt. A task that repeats over many items: do one first, check it, then the rest. Before you finish, compare the result with what was asked.
|
|
17
18
|
{{#if caller}}Commands run in the folder {{root}}, and relative paths are relative to it.{{else}}Paths are relative to the project folder, and commands already run in it. The project folder is {{root}} (for file:// URLs in the browser); stay inside it: don't search the rest of the disk.
|
|
18
19
|
When you are done, reply with a short summary of what you did (or the answer to the question) and stop calling tools.{{/if}}
|
|
19
20
|
|
|
@@ -170,13 +171,13 @@ Which of your models does the job (from "Your other models" in the system prompt
|
|
|
170
171
|
Besides you (tool calls, {{you}}), the user set up these models:
|
|
171
172
|
{{models}}
|
|
172
173
|
|
|
173
|
-
Work with them through the agent tool: "model" picks one
|
|
174
|
+
Work with them through the agent tool: "model" picks one, "images" gives it images to look at.
|
|
174
175
|
|
|
175
176
|
- Never decline, guess or work around something because you can't do it (see an image, think it through, read more than fits your context): ask a model that can. Choose by what the job needs: the ability first; a faster one for a simple job, the strongest for a hard one; not one that keeps failing.
|
|
176
177
|
- Ask for what you need in order to act, not for a description. Not "describe this screenshot" but "Which of these 9 tiles (numbered 1-9, left to right, top to bottom) show a chair? Reply with the numbers only." or "Where is the Submit button? Reply with its exact text and its position (top/middle/bottom, left/center/right)." Say what to answer and in what form, and nothing more.
|
|
177
178
|
- Many images, or a job in independent parts: one agent call per image or part, all in the same reply (the same model several times, or different ones). Then put the answers together and decide the next step yourself.
|
|
178
179
|
- A loop that needs the ability at every step (look, act, look again, until done) goes whole to a model that has it: the goal, how to tell it's done, what to report. Better than asking about each step.
|
|
179
|
-
-
|
|
180
|
+
- Give a helper all it needs: the goal, what's known (paths, URLs, the page the browser is on, what was tried), what to reply. It has your tools, the same browser and MCP servers too: helpers that need the same MCP server (a browser) take turns (HeartCode makes the next one wait), so jobs that use it don't gain from running together; and never give two helpers the same files at once.
|
|
180
181
|
- A helper that fails or replies with nothing is handed on to your next model that can do the job, on its own. An answer that doesn't settle it (wrong, unsure, part of it): ask again, a sharper question or another capable model, until it's done.
|
|
181
182
|
- Trust a helper's answer to what you asked; question it only when it looks doubtful (it contradicts what you know, or it's vague) or acting on it can't be undone. Don't redo its work yourself.
|
|
182
183
|
|
|
@@ -188,7 +189,7 @@ You can see images: those in the user's messages, those tools return (a browser
|
|
|
188
189
|
## tool-agent-images
|
|
189
190
|
|
|
190
191
|
<!-- The agent tool's "images", there when the user has other models. -->
|
|
191
|
-
Images for the helper to look at: image file paths (in the project, saved screenshots, ones the user gave) or image URLs. The model must see images
|
|
192
|
+
Images for the helper to look at: image file paths (in the project, saved screenshots, ones the user gave) or image URLs. The model must see images.
|
|
192
193
|
|
|
193
194
|
## images-unseen
|
|
194
195
|
|
|
@@ -324,7 +325,7 @@ Update a note rather than adding a duplicate, and link related notes with [[name
|
|
|
324
325
|
## tool-memory
|
|
325
326
|
|
|
326
327
|
<!-- The memory tool's description. -->
|
|
327
|
-
Memory notes. action "write": name, description (one line), type (user, project, feedback or reference), tags (1-3), scope ("project", the default, or "global"
|
|
328
|
+
Memory notes. action "write": name, description (one line), type (user, project, feedback or reference), tags (1-3), scope ("project", the default, or "global"), content (markdown; link notes with [[name]]). "read": name. "search": query (words, or tag:name). "delete": name (and scope if the name is in both).
|
|
328
329
|
|
|
329
330
|
## memory-extract
|
|
330
331
|
|
package/src/agent.js
CHANGED
|
@@ -695,9 +695,12 @@ async function runNative(s, task, images, signal) {
|
|
|
695
695
|
// shorter conversation fits (a per-minute cap smaller than that), so not tried again
|
|
696
696
|
const fixed = Math.round((est(JSON.stringify(tools)) + sizeOf(s.messages[0])) * (strat.ratio ?? 1))
|
|
697
697
|
if (fixed >= e.inputLimit) throw Object.assign(e, { message: `${e.message} (the system prompt and tools alone are ~${fixed} tokens: this can't fit)` })
|
|
698
|
-
s.sizeCap = Math.min(s.sizeCap ?? Infinity, e.inputLimit)
|
|
699
698
|
out.end()
|
|
700
|
-
|
|
699
|
+
// e.fitted: the next model's window is smaller (strat already follows it): compacted at once
|
|
700
|
+
if (!e.fitted) {
|
|
701
|
+
s.sizeCap = Math.min(s.sizeCap ?? Infinity, e.inputLimit)
|
|
702
|
+
shrink *= 0.8 // strat.context already dropped to the limit; leave more headroom each time
|
|
703
|
+
}
|
|
701
704
|
step--
|
|
702
705
|
continue
|
|
703
706
|
}
|
package/src/client.js
CHANGED
|
@@ -184,12 +184,34 @@ export class Client {
|
|
|
184
184
|
this.#next(why)
|
|
185
185
|
return true
|
|
186
186
|
}
|
|
187
|
-
#next(why) {
|
|
187
|
+
#next(why, pick) {
|
|
188
188
|
const r = this.rotation
|
|
189
|
-
r.at = (r.at + 1) % r.list.length
|
|
189
|
+
r.at = pick ? pick() : (r.at + 1) % r.list.length
|
|
190
190
|
log('rotate', `${why} → ${r.list[r.at].provider} · ${r.list[r.at].model}`)
|
|
191
191
|
this.#use(r.list[r.at])
|
|
192
192
|
}
|
|
193
|
+
// Which model of the list to go on to after this one failed, for one request (tried: the places
|
|
194
|
+
// tried, down: the providers that answered with an error, 400 or above). A provider that answered
|
|
195
|
+
// with an error isn't asked again through its next model: the next one is from another provider.
|
|
196
|
+
// When every provider has failed, the models not tried yet, in order; when all have been tried,
|
|
197
|
+
// simply the next one (round again).
|
|
198
|
+
#pick(e, tried, down) {
|
|
199
|
+
const r = this.rotation
|
|
200
|
+
tried.add(r.at)
|
|
201
|
+
if (e?.status >= 400) down.add(r.list[r.at].provider)
|
|
202
|
+
const n = r.list.length
|
|
203
|
+
const after = (ok) => {
|
|
204
|
+
for (let k = 1; k <= n; k++) if (ok((r.at + k) % n)) return (r.at + k) % n
|
|
205
|
+
return -1
|
|
206
|
+
}
|
|
207
|
+
let i = after((i) => !tried.has(i) && !down.has(r.list[i].provider))
|
|
208
|
+
if (i < 0) i = after((i) => !tried.has(i))
|
|
209
|
+
if (i < 0) {
|
|
210
|
+
tried.clear() // all tried: round again
|
|
211
|
+
i = (r.at + 1) % n
|
|
212
|
+
}
|
|
213
|
+
return i
|
|
214
|
+
}
|
|
193
215
|
// A new message: back to #1 (unless it stays on the one that worked)
|
|
194
216
|
newMessage() {
|
|
195
217
|
const r = this.rotation
|
|
@@ -203,11 +225,11 @@ export class Client {
|
|
|
203
225
|
if (changed) this.onUse?.(t)
|
|
204
226
|
}
|
|
205
227
|
// After a failure: go on to the next model? (then it's tried at once)
|
|
206
|
-
#rotate(e, tries) {
|
|
228
|
+
#rotate(e, tries, pick) {
|
|
207
229
|
const r = this.rotation
|
|
208
230
|
if (!r) return false
|
|
209
231
|
if (!(r.on === 'any' || (r.on === 'failure' && (outage(e) || unusable(e))) || (r.on === 'retries' && (tries >= r.retries || unusable(e))))) return false
|
|
210
|
-
r.at = (r.at + 1) % r.list.length
|
|
232
|
+
r.at = pick ? pick() : (r.at + 1) % r.list.length
|
|
211
233
|
log('rotate', `${e.message.slice(0, 200)} → ${r.list[r.at].provider} · ${r.list[r.at].model}`)
|
|
212
234
|
this.#use(r.list[r.at])
|
|
213
235
|
return true
|
|
@@ -284,6 +306,8 @@ export class Client {
|
|
|
284
306
|
// The server's own entries for its models ({ id, owned_by, context_length, pricing… }, as it sends them).
|
|
285
307
|
async modelEntries(timeout = 10_000) {
|
|
286
308
|
const data = await this.#json(this.baseUrl + '/models', undefined, timeout)
|
|
309
|
+
// { result: [...] } (Cloudflare's shape): there the id is an internal one and the name is the model's
|
|
310
|
+
if (Array.isArray(data.result)) return data.result.filter((m) => m?.name).map((m) => ({ ...m, id: m.name }))
|
|
287
311
|
return (data.data ?? data.models ?? []).filter((m) => m && (m.id ?? m.name))
|
|
288
312
|
}
|
|
289
313
|
|
|
@@ -399,7 +423,17 @@ export class Client {
|
|
|
399
423
|
let round = 0 // models tried since the last wait (a whole round of them failing: a short pause)
|
|
400
424
|
let hard = true // every failure in this round was one that waiting can't fix (see outage)
|
|
401
425
|
let refusedBy = 0 // models that refused this request (a bad request), tried in turn
|
|
426
|
+
const tried = new Set() // places in the model list tried for this request
|
|
427
|
+
const down = new Set() // providers that answered it with an error (400 or above): their other models are skipped
|
|
428
|
+
const first = this.target
|
|
402
429
|
for (let attempt = 0; ; attempt++) {
|
|
430
|
+
// moved on to a model with a smaller window than the conversation was fitted to: handed back
|
|
431
|
+
// (as "too large", at that window) so the caller summarizes it first, instead of a refusal
|
|
432
|
+
const sent = this.target !== first && body.messages && this.can?.context && Math.round(estMessages(body.messages) * this.tokenRatio)
|
|
433
|
+
if (sent > this.can.context * 0.9) {
|
|
434
|
+
log('limit', `${this.model}: ~${sent} tokens don't fit its window (${this.can.context}); handed back to be made smaller`)
|
|
435
|
+
throw Object.assign(new Error(`the conversation (~${sent} tokens) is too large for ${this.model} (${this.can.context})`), { inputLimit: this.can.context, fitted: true })
|
|
436
|
+
}
|
|
403
437
|
try {
|
|
404
438
|
const r = await this.#timed(body, { ...opts, once: true })
|
|
405
439
|
if (attempt) this.onRetry(null)
|
|
@@ -415,7 +449,7 @@ export class Client {
|
|
|
415
449
|
// conversation holds from another model). Two more at most, then it stops.
|
|
416
450
|
if (!outage(e) && !unusable(e) && this.rotation && this.rotation.on !== 'any' && refusedBy < Math.min(2, this.rotation.list.length - 1)) {
|
|
417
451
|
refusedBy++
|
|
418
|
-
this.#next(`refused (${e.status})
|
|
452
|
+
this.#next(`refused (${e.status})`, () => this.#pick(e, tried, down))
|
|
419
453
|
this.onRetry(e, 0, attempt + 1, this.target)
|
|
420
454
|
continue
|
|
421
455
|
}
|
|
@@ -426,7 +460,7 @@ export class Client {
|
|
|
426
460
|
if (!outage(e) && !(unusable(e) && this.rotation) && this.rotation?.on !== 'any') throw e
|
|
427
461
|
hard &&= !outage(e)
|
|
428
462
|
tries++
|
|
429
|
-
if (this.#rotate(e, tries)) {
|
|
463
|
+
if (this.#rotate(e, tries, () => this.#pick(e, tried, down))) {
|
|
430
464
|
tries = 0
|
|
431
465
|
// ponytail: a 1s pause after a whole round fails (no connection at all: not thousands of
|
|
432
466
|
// requests a minute); the user chose "round again at once"
|
package/src/settings.html
CHANGED
|
@@ -1254,7 +1254,7 @@ function drawProviders(s) {
|
|
|
1254
1254
|
// left: every provider's models, A to Z; one click adds it (the first one becomes #1). Built
|
|
1255
1255
|
// again only when what it lists changes (a provider, its models, the search); a change to your
|
|
1256
1256
|
// list only updates the rows it touches (markCatalog), so the list stays where you scrolled it.
|
|
1257
|
-
const shape = JSON.stringify([s.providers, s.providers.map((p) => (MODELS[p.name]?.list ?? []).length + (MODELS[p.name]?.error ?? '') + (MODELS[p.name]
|
|
1257
|
+
const shape = JSON.stringify([s.providers, s.providers.map((p) => (MODELS[p.name]?.list ?? []).length + (MODELS[p.name]?.error ?? '') + ((MODELS[p.name] ?? { loading: true }).loading ? 'loading' : '')), modelQuery.trim(), catSort])
|
|
1258
1258
|
// every provider's list at once: on when all are
|
|
1259
1259
|
const allOn = s.providers.length > 0 && s.providers.every((p) => p.list)
|
|
1260
1260
|
put($('lists-all'), [sw({ checked: allOn, 'aria-label': "load every provider's model list", onchange: (e) => act('/api/provider/list', { on: e.target.checked }, `every provider's model list ${e.target.checked ? 'on' : 'off'}`) }), 'Load all lists'])
|
package/src/settings.js
CHANGED
|
@@ -427,7 +427,7 @@ export function startSettings({ cfg, apply = () => {}, port = 0 } = {}) {
|
|
|
427
427
|
if (!p) throw new Error(`no provider "${name}"`)
|
|
428
428
|
if (p.list === false) return { models: [], details: {}, off: true }
|
|
429
429
|
const lists = readJson(LISTS, {})
|
|
430
|
-
let saved = lists[name]?.baseUrl === p.baseUrl ? lists[name] : null
|
|
430
|
+
let saved = lists[name]?.baseUrl === p.baseUrl && lists[name].models?.length ? lists[name] : null // an empty one: asked again
|
|
431
431
|
if (!saved) {
|
|
432
432
|
const entries = await new Client({ baseUrl: p.baseUrl, apiKey: p.apiKey }).modelEntries()
|
|
433
433
|
const num = (...xs) => xs.map(Number).find((x) => x > 0)
|