@hncode/hncode 0.5.2 → 0.5.5
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 +38 -38
- package/src/agent.js +172 -19
- package/src/cache.js +175 -130
- package/src/clipboard.js +69 -2
- package/src/config.js +10 -3
- package/src/index.js +29 -5
- package/src/keybindings.js +25 -3
- package/src/llm.js +54 -6
- package/src/mcp.js +419 -384
- package/src/plugin.js +95 -10
- package/src/remote.js +307 -289
- package/src/session-hub.js +37 -0
- package/src/settings-schema.js +2 -0
- package/src/shell-safety.js +610 -562
- package/src/swarm-progress.js +1 -1
- package/src/team/mailbox.js +216 -139
- package/src/team/permission.js +121 -0
- package/src/team/protocol.js +167 -0
- package/src/team/runner.js +127 -12
- package/src/team/spawn.js +63 -5
- package/src/team/store.js +31 -4
- package/src/team/tasks.js +15 -2
- package/src/team/team.js +51 -16
- package/src/team-panel.js +418 -20
- package/src/term-caps.js +457 -410
- package/src/term.js +142 -26
- package/src/tool-result.js +50 -2
- package/src/tools/agent.js +464 -436
- package/src/tools/bash.js +53 -14
- package/src/tools/diagnostics.js +13 -0
- package/src/tools/edit.js +727 -577
- package/src/tools/fetch-url.js +118 -5
- package/src/tools/glob.js +5 -3
- package/src/tools/grep.js +19 -5
- package/src/tools/ignore.js +17 -3
- package/src/tools/index.js +17 -3
- package/src/tools/read.js +13 -2
- package/src/tools/shell.js +145 -134
- package/src/tools/tasks.js +214 -141
- package/src/tools/team.js +13 -0
- package/src/tools/utils.js +12 -3
- package/src/tools/web-search.js +85 -34
- package/src/tui.js +2089 -714
- package/src/update.js +38 -3
- package/src/web.js +47 -1
package/src/llm.js
CHANGED
|
@@ -93,7 +93,7 @@ export function toOpenAi(messages) {
|
|
|
93
93
|
} else if (m.role === 'assistant') {
|
|
94
94
|
const hasText = typeof m.content === 'string' && m.content.trim();
|
|
95
95
|
const hasCalls = Array.isArray(m.toolCalls) && m.toolCalls.length;
|
|
96
|
-
|
|
96
|
+
if (!hasText && !hasCalls) continue; // the provider rejects an empty assistant turn; drop it
|
|
97
97
|
const msg = { role: 'assistant', content: m.content || null };
|
|
98
98
|
if (hasCalls) {
|
|
99
99
|
msg.tool_calls = m.toolCalls.map((tc) => ({
|
|
@@ -296,7 +296,9 @@ function buildBody(cfg, messages, streaming = true, opts = {}) {
|
|
|
296
296
|
model: cfg.innerModel, messages: toOpenAi(messages),
|
|
297
297
|
...common, ...wire,
|
|
298
298
|
};
|
|
299
|
-
|
|
299
|
+
// Only attach a NON-EMPTY tools array: `tools: []` is rejected by some strict
|
|
300
|
+
// gateways, and an instance that registered no list means "no tools here".
|
|
301
|
+
if (!opts.noTools && defs.length) body.tools = defs;
|
|
300
302
|
return JSON.stringify(applyPromptCache('openai', body, {
|
|
301
303
|
enabled: cfg.promptCache !== false,
|
|
302
304
|
sessionKey: cfg.sessionId,
|
|
@@ -313,6 +315,12 @@ export class LLM {
|
|
|
313
315
|
constructor(cfg) {
|
|
314
316
|
this.cfg = cfg;
|
|
315
317
|
this.controller = null;
|
|
318
|
+
// The tool defs THIS instance sends. Per-instance, NOT module-global: the
|
|
319
|
+
// global (`llmToolsList` below) was overwritten by every `new Agent()` in
|
|
320
|
+
// the process, so a subagent, swarm member or teammate constructed while a
|
|
321
|
+
// parent request was building its body silently swapped the parent's tool
|
|
322
|
+
// list mid-turn. The agent sets this via setTools() right after construction.
|
|
323
|
+
this._tools = null;
|
|
316
324
|
// Inline-reasoning state carried ACROSS requests. Our system prompt tells the
|
|
317
325
|
// model to put all its reasoning inside one <think> span, and tool calls happen
|
|
318
326
|
// inside that span — so the span outlives a single request. Reset when the turn
|
|
@@ -321,6 +329,11 @@ export class LLM {
|
|
|
321
329
|
this._tagPending = '';
|
|
322
330
|
}
|
|
323
331
|
|
|
332
|
+
// Register the tool list this instance advertises (see `_tools`). Only ever
|
|
333
|
+
// store an array or null: a non-array would make every later request throw
|
|
334
|
+
// when the defs are built.
|
|
335
|
+
setTools(list) { this._tools = Array.isArray(list) ? list : null; }
|
|
336
|
+
|
|
324
337
|
// Called by the agent at the end of a turn: the next turn starts a fresh
|
|
325
338
|
// reasoning block, even if the previous one never saw its closing tag.
|
|
326
339
|
resetThink() {
|
|
@@ -341,13 +354,29 @@ export class LLM {
|
|
|
341
354
|
// a capped budget plus a reasoning model is what silently produced an empty
|
|
342
355
|
// title (see buildBody), and an internal request has no tools to call.
|
|
343
356
|
async requestText(messages, opts = {}) {
|
|
357
|
+
// One silent retry on FAILURE (null). These one-shot requests (title,
|
|
358
|
+
// compaction summary) have no outer retry loop, and a single transient
|
|
359
|
+
// 429/network blip used to come back as null — which the compaction path
|
|
360
|
+
// treated as "no summary" and trimmed the history over. `''` (capped-empty)
|
|
361
|
+
// is a meaningful verdict for the caller and is NOT retried here.
|
|
362
|
+
let out = await this._requestTextOnce(messages, opts);
|
|
363
|
+
if (out === null && !(this.controller && this.controller.signal.aborted)) {
|
|
364
|
+
await new Promise((r) => setTimeout(r, 400));
|
|
365
|
+
out = await this._requestTextOnce(messages, opts);
|
|
366
|
+
}
|
|
367
|
+
return out;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
async _requestTextOnce(messages, opts = {}) {
|
|
344
371
|
const cfg = this.cfg;
|
|
345
372
|
this.controller = new AbortController();
|
|
373
|
+
// This instance's tools, unless the caller opted out (internal one-shots).
|
|
374
|
+
const bodyOpts = { ...opts, ...((this._tools && !opts.noTools) ? { tools: this._tools } : {}) };
|
|
346
375
|
try {
|
|
347
376
|
const res = await fetch(cfg.endpoint, {
|
|
348
377
|
method: 'POST',
|
|
349
378
|
headers: (() => { const h = authHeaders(cfg); try { const out = runPatchSync('llmHeaders', { headers: h }, (c) => c); if (out && out.headers) return out.headers; } catch (e) {} return h; })(),
|
|
350
|
-
body: (() => { let b = buildBody(cfg, messages, false,
|
|
379
|
+
body: (() => { let b = buildBody(cfg, messages, false, bodyOpts); try { const out = runPatchSync('llmBody', { body: b, cfg }, (c) => c); if (out && out.body !== undefined) b = out.body; } catch (e) {} return b; })(),
|
|
351
380
|
signal: this.controller.signal,
|
|
352
381
|
});
|
|
353
382
|
if (!res || !res.ok) {
|
|
@@ -397,7 +426,7 @@ export class LLM {
|
|
|
397
426
|
}
|
|
398
427
|
}
|
|
399
428
|
|
|
400
|
-
async request(messages, onEvent) {
|
|
429
|
+
async request(messages, onEvent, cacheEnd) {
|
|
401
430
|
// Mixin seam: let plugins mutate the messages sent to the model.
|
|
402
431
|
try { const out = await runPatch('llmRequest', { messages, cfg: this.cfg }, (c) => c); if (out && out.messages) messages = out.messages; } catch (e) { /* patch failed; proceed with original */ }
|
|
403
432
|
const cfg = this.cfg;
|
|
@@ -418,6 +447,11 @@ export class LLM {
|
|
|
418
447
|
// turns it off and the retry goes out without it — degrading to an estimate
|
|
419
448
|
// rather than failing the request over an accounting nicety.
|
|
420
449
|
const reqOpts = {};
|
|
450
|
+
// This instance's tools ride along on every attempt (per-instance; see the
|
|
451
|
+
// constructor note on `_tools`). `tools` is only included when set, so
|
|
452
|
+
// buildBody's hasOwnProperty probe falls through to the global for callers
|
|
453
|
+
// that never registered a list.
|
|
454
|
+
const instanceTools = this._tools ? { tools: this._tools } : {};
|
|
421
455
|
|
|
422
456
|
for (let attempt = 1; attempt <= RETRIES; attempt++) {
|
|
423
457
|
attempts = attempt;
|
|
@@ -435,7 +469,7 @@ export class LLM {
|
|
|
435
469
|
res = await fetch(cfg.endpoint, {
|
|
436
470
|
method: 'POST',
|
|
437
471
|
headers: (() => { const h = authHeaders(cfg); try { const out = runPatchSync('llmHeaders', { headers: h }, (c) => c); if (out && out.headers) return out.headers; } catch (e) {} return h; })(),
|
|
438
|
-
body: (() => { let b = buildBody(cfg, messages, true, reqOpts); try { const out = runPatchSync('llmBody', { body: b, cfg }, (c) => c); if (out && out.body !== undefined) b = out.body; } catch (e) {} return b; })(),
|
|
472
|
+
body: (() => { let b = buildBody(cfg, messages, true, { ...reqOpts, cacheEnd, ...instanceTools }); try { const out = runPatchSync('llmBody', { body: b, cfg }, (c) => c); if (out && out.body !== undefined) b = out.body; } catch (e) {} return b; })(),
|
|
439
473
|
signal,
|
|
440
474
|
});
|
|
441
475
|
} catch (e) {
|
|
@@ -452,6 +486,10 @@ export class LLM {
|
|
|
452
486
|
if (res.status === 400 && reqOpts.includeUsage !== false) reqOpts.includeUsage = false;
|
|
453
487
|
}
|
|
454
488
|
if (attempt < RETRIES) {
|
|
489
|
+
// Release the FAILED attempt's body so its socket returns to the pool.
|
|
490
|
+
// An unread body pins the connection until GC — under a sustained
|
|
491
|
+
// 429/5xx storm the retries accumulated one open connection per attempt.
|
|
492
|
+
if (res && res.body) { try { await res.body.cancel(); } catch { /* already gone */ } }
|
|
455
493
|
// Small exponential backoff (200ms, 400ms). If the user hit Esc during
|
|
456
494
|
// the wait, stop retrying.
|
|
457
495
|
const tryingSignal = this.controller.signal;
|
|
@@ -475,7 +513,17 @@ export class LLM {
|
|
|
475
513
|
return;
|
|
476
514
|
}
|
|
477
515
|
if (cfg.stream === false) {
|
|
478
|
-
|
|
516
|
+
// A 200 with a malformed/empty body used to reject here, and the caller
|
|
517
|
+
// (an unawaited `runAgent`) turned that rejection into a process exit via
|
|
518
|
+
// the global unhandledRejection handler. Surface it as an error EVENT —
|
|
519
|
+
// the same channel every other failure on this path uses.
|
|
520
|
+
let json;
|
|
521
|
+
try {
|
|
522
|
+
json = await res.json();
|
|
523
|
+
} catch (e) {
|
|
524
|
+
onEvent({ type: 'error', error: new Error(`LLM returned a malformed non-streaming body: ${e && e.message}`) });
|
|
525
|
+
return;
|
|
526
|
+
}
|
|
479
527
|
return consumeNonStreaming(cfg.protocol, json, onEvent);
|
|
480
528
|
}
|
|
481
529
|
if (!res.body) { onEvent({ type: 'end' }); return; }
|