billion-context-pi 0.1.53 → 0.1.54-pr.237.54
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 +18 -0
- package/README.zh-CN.md +18 -0
- package/dist/delegate-tool.d.ts +10 -2
- package/dist/index.js +190 -61
- package/dist/index.js.map +1 -1
- package/dist/omp.d.ts +19 -0
- package/dist/runtime.d.ts +5 -0
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -22,6 +22,8 @@ The model decides <em>when</em> and <em>what</em> to compress — not a hard lim
|
|
|
22
22
|
|
|
23
23
|
---
|
|
24
24
|
|
|
25
|
+
> **Host support:** this plugin is for **Pi**. It does **not** support **OMP (oh-my-pi)** — on an OMP host it refuses to run. OMP users: use [billion-context](https://github.com/ranxianglei/billion-context) instead (`bili omp`). Details: [docs/omp.md](./docs/omp.md).
|
|
26
|
+
|
|
25
27
|
## Why?
|
|
26
28
|
|
|
27
29
|
When conversations get long, the model runs out of context. Most tools hard-truncate — silently dropping earlier messages. **billion-context** gives the model a `compress` tool: the LLM decides **when** and **what** to compress into high-fidelity summaries, preserving critical details (file paths, decisions, error strings) while reclaiming context space.
|
|
@@ -72,6 +74,22 @@ This has two practical implications:
|
|
|
72
74
|
|
|
73
75
|
2. **Even with a single compression plugin, interference is still possible in rare cases.** Load order under Pi is determined by filesystem discovery order (`fs.readdirSync` over `.pi/extensions/` → global → packages), which is not fully deterministic. If another (non-compression) extension also hooks the `context` event and happens to load *after* billion-context-pi, it could modify the compressed output. billion-context-pi rebuilds its working set from the session log rather than the chained input, which makes it robust to handlers that run *before* it — but it cannot defend against a handler that runs *after* it. This is a limitation of Pi's extension model; if you observe unexpected context behavior, check whether other installed extensions intercept the `context` event.
|
|
74
76
|
|
|
77
|
+
## Host support
|
|
78
|
+
|
|
79
|
+
billion-context-pi is built for the **Pi** coding agent (`@earendil-works/pi-coding-agent`) and detects the host at session start:
|
|
80
|
+
|
|
81
|
+
- **Pi** — fully supported.
|
|
82
|
+
- **OMP (`can1357/oh-my-pi`)** — **not supported.** OMP's in-process session API diverges from Pi's, so the compression refs the extension injects can drift out of sync with the session's real refs and `compress` calls fail with `does not exist in this session` (issue [#234](https://github.com/ranxianglei/billion-context-pi/issues/234)). On OMP the extension now **refuses service**: it prints a warning, disables the ACP tools, and leaves the host's own context handling untouched.
|
|
83
|
+
|
|
84
|
+
**Use [billion-context](https://github.com/ranxianglei/billion-context) instead** — it runs the same compression pipeline server-side in a proxy, so the refs never diverge:
|
|
85
|
+
|
|
86
|
+
```bash
|
|
87
|
+
npm install -g billion-context
|
|
88
|
+
bili omp # run OMP through the proxy
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
Full details: [docs/omp.md](./docs/omp.md).
|
|
92
|
+
|
|
75
93
|
## Model-facing tools
|
|
76
94
|
|
|
77
95
|
| Tool | What it does |
|
package/README.zh-CN.md
CHANGED
|
@@ -20,6 +20,8 @@
|
|
|
20
20
|
|
|
21
21
|
---
|
|
22
22
|
|
|
23
|
+
> **宿主支持:** 本插件面向 **Pi**。它**不支持 OMP(oh-my-pi)** —— 在 OMP 宿主上会拒绝运行。OMP 用户请改用 [billion-context](https://github.com/ranxianglei/billion-context)(`bili omp`)。详细说明:[docs/omp.zh-CN.md](./docs/omp.zh-CN.md)。
|
|
24
|
+
|
|
23
25
|
## 为什么选择 billion-context
|
|
24
26
|
|
|
25
27
|
当对话变长,模型的上下文会耗尽。多数工具采用硬截断 —— 静默丢弃早期消息。**billion-context** 把 `compress` 工具交给模型:由 LLM 决定**何时**压缩、压缩**什么**,将内容压缩成高保真摘要,在回收上下文空间的同时保留关键细节(文件路径、决策、错误字符串)。
|
|
@@ -71,6 +73,22 @@ billion-context 通过拦截 Pi 的 `context` 事件接管上下文管理。**Pi
|
|
|
71
73
|
|
|
72
74
|
2. **即使只有一个压缩插件,在少数情况下仍可能出现干扰。** Pi 下的加载顺序由文件系统发现顺序(`fs.readdirSync` 遍历 `.pi/extensions/` → 全局 → 包)决定,并不完全确定。如果另一个(非压缩类)扩展也 hook 了 `context` 事件、且恰好加载在 billion-context-pi *之后*,它可能修改压缩后的输出。billion-context-pi 从会话日志重建工作集(而非链式输入),这让它对*排在它之前*的 handler 鲁棒 —— 但无法防御*排在它之后*的 handler。这是 Pi 扩展模型的固有限制;若你观察到上下文行为异常,请检查是否有其他已安装扩展拦截了 `context` 事件。
|
|
73
75
|
|
|
76
|
+
## 宿主支持
|
|
77
|
+
|
|
78
|
+
billion-context-pi 面向 **Pi** 编码代理(`@earendil-works/pi-coding-agent`)构建,并在会话开始时检测宿主:
|
|
79
|
+
|
|
80
|
+
- **Pi** — 完全支持。
|
|
81
|
+
- **OMP(`can1357/oh-my-pi`)** — **不支持。** OMP 的进程内会话 API 与 Pi 不同,扩展注入的压缩引用可能与会话的真实引用漂移失步,导致 `compress` 调用失败,报错 `does not exist in this session`(issue [#234](https://github.com/ranxianglei/billion-context-pi/issues/234))。在 OMP 上,扩展现在会**拒绝服务**:打印警告、禁用 ACP 工具,并保持宿主自身的上下文处理不受影响。
|
|
82
|
+
|
|
83
|
+
**请改用 [billion-context](https://github.com/ranxianglei/billion-context)** — 它把同样的压缩流水线运行在服务端代理里,因此引用不会漂移:
|
|
84
|
+
|
|
85
|
+
```bash
|
|
86
|
+
npm install -g billion-context
|
|
87
|
+
bili omp # 让 OMP 通过代理运行
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
完整说明:[docs/omp.zh-CN.md](./docs/omp.zh-CN.md)。
|
|
91
|
+
|
|
74
92
|
## 模型工具
|
|
75
93
|
|
|
76
94
|
| 工具 | 作用 |
|
package/dist/delegate-tool.d.ts
CHANGED
|
@@ -8,7 +8,7 @@ export declare function delegateSpawnOptions(cwd: string, env: NodeJS.ProcessEnv
|
|
|
8
8
|
* run the SDK inside another node process, so probe instead. Non-pi hosts
|
|
9
9
|
* (omp) keep argv[1] untouched. */
|
|
10
10
|
export declare function resolvePiCliEntry(argv1: string, env?: NodeJS.ProcessEnv, piHost?: boolean): string;
|
|
11
|
-
type RunStatus = "running" | "completed" | "failed" | "cancelled";
|
|
11
|
+
export type RunStatus = "running" | "completed" | "failed" | "cancelled";
|
|
12
12
|
interface DelegateRun {
|
|
13
13
|
runId: string;
|
|
14
14
|
agent: string;
|
|
@@ -104,6 +104,7 @@ declare const WaitParams: Type.TObject<{
|
|
|
104
104
|
}>;
|
|
105
105
|
export declare function accumulateUsage(a: Usage | undefined, b: Usage): Usage;
|
|
106
106
|
export declare function makeDelegateTool(pi: ExtensionAPI): ToolDefinition<typeof DelegateParams>;
|
|
107
|
+
export declare function formatRunResult(run: DelegateRun): string;
|
|
107
108
|
/** Runs that reached a terminal state but whose result never reached the
|
|
108
109
|
* model: no parked waiter, not consumed by a tool result, and never injected
|
|
109
110
|
* as a notification. The model was promised a notification ("do NOT keep
|
|
@@ -163,5 +164,12 @@ export declare function buildChildArgs(args: DelegateArgs, rolePrompt: string, c
|
|
|
163
164
|
isAsync: boolean;
|
|
164
165
|
useJsonStream: boolean;
|
|
165
166
|
}>;
|
|
166
|
-
|
|
167
|
+
/** Watchdog/EOF finalize arrives with code === null (the child was killed or
|
|
168
|
+
* never exited). If a result was delivered (non-empty reply or stderr), the
|
|
169
|
+
* run counts as completed (0); otherwise it stays null = genuine failure. */
|
|
170
|
+
export declare function effectiveExitCode(code: number | null, output: string, stderr: string): number | null;
|
|
171
|
+
/** status (set by finalize from the effective exit code) is the authority for
|
|
172
|
+
* the FAILED/completed decision; the raw code is diagnostic display only
|
|
173
|
+
* ("exit ?"), so the notification can never disagree with run.status. */
|
|
174
|
+
export declare function injectResult(pi: ExtensionAPI, agent: string, runId: string, task: string, status: RunStatus, code: number | null, file: string, timedOut?: string, usage?: Usage, mode?: "merged" | "separate", usageAlreadyReported?: boolean, body?: string): boolean;
|
|
167
175
|
export {};
|
package/dist/index.js
CHANGED
|
@@ -424,18 +424,42 @@ function assignRefs(messages, options) {
|
|
|
424
424
|
return { map, nextIndex: cursor, newlyAssigned };
|
|
425
425
|
}
|
|
426
426
|
function allocateFreeRef(map, start) {
|
|
427
|
-
|
|
428
|
-
|
|
427
|
+
const from = Math.max(start, MIN_INDEX);
|
|
428
|
+
for (let candidate = from; candidate <= MAX_INDEX; candidate++) {
|
|
429
|
+
const text = indexToRef(candidate);
|
|
430
|
+
if (!map.byRef[text]) {
|
|
431
|
+
return { text, index: candidate };
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
for (let candidate = MIN_INDEX; candidate < from; candidate++) {
|
|
429
435
|
const text = indexToRef(candidate);
|
|
430
436
|
if (!map.byRef[text]) {
|
|
431
437
|
return { text, index: candidate };
|
|
432
438
|
}
|
|
433
|
-
candidate++;
|
|
434
439
|
}
|
|
435
440
|
throw new Error(
|
|
436
|
-
`ref capacity exhausted:
|
|
441
|
+
`ref capacity exhausted: all ${MAX_INDEX} ref slots are occupied by live messages \u2014 nothing left to reclaim`
|
|
437
442
|
);
|
|
438
443
|
}
|
|
444
|
+
function pruneDeadRefs(map, liveIds) {
|
|
445
|
+
let pruned = 0;
|
|
446
|
+
const byRaw = {};
|
|
447
|
+
for (const [rawId, ref] of Object.entries(map.byRaw)) {
|
|
448
|
+
if (liveIds.has(rawId)) {
|
|
449
|
+
byRaw[rawId] = ref;
|
|
450
|
+
continue;
|
|
451
|
+
}
|
|
452
|
+
pruned++;
|
|
453
|
+
}
|
|
454
|
+
return { map: rebuildRefIndex({ byRaw, byRef: {} }), pruned };
|
|
455
|
+
}
|
|
456
|
+
function rebuildRefIndex(map) {
|
|
457
|
+
const byRef = {};
|
|
458
|
+
for (const [rawId, ref] of Object.entries(map.byRaw)) {
|
|
459
|
+
if (ref !== BLOCKED_REF) byRef[ref] = rawId;
|
|
460
|
+
}
|
|
461
|
+
return { byRaw: { ...map.byRaw }, byRef };
|
|
462
|
+
}
|
|
439
463
|
function highestUsedIndex(map) {
|
|
440
464
|
let highest = 0;
|
|
441
465
|
for (const ref of Object.values(map.byRaw)) {
|
|
@@ -634,6 +658,13 @@ function syncBlocks(messages, state) {
|
|
|
634
658
|
deactivated.push(block.blockId);
|
|
635
659
|
}
|
|
636
660
|
}
|
|
661
|
+
const liveIds = new Set(presentIds);
|
|
662
|
+
for (const block of result.blocks) {
|
|
663
|
+
if (!block.active) continue;
|
|
664
|
+
for (const id of block.effectiveMessageIds) liveIds.add(id);
|
|
665
|
+
}
|
|
666
|
+
const prunedRefs = pruneDeadRefs(result.messageRefs, liveIds);
|
|
667
|
+
result.messageRefs = prunedRefs.map;
|
|
637
668
|
return { state: result, deactivated };
|
|
638
669
|
}
|
|
639
670
|
function defaultConfig(modelContextLimit, overrides = {}) {
|
|
@@ -744,12 +775,14 @@ var BoundaryNotFoundError = class extends Error {
|
|
|
744
775
|
code = "BOUNDARY_NOT_FOUND";
|
|
745
776
|
kind;
|
|
746
777
|
endpoint;
|
|
747
|
-
|
|
778
|
+
reason;
|
|
779
|
+
constructor(kind, endpoint, message, reason = null) {
|
|
748
780
|
super(message);
|
|
749
781
|
this.name = "BoundaryNotFoundError";
|
|
750
782
|
this.code = "BOUNDARY_NOT_FOUND";
|
|
751
783
|
this.kind = kind;
|
|
752
784
|
this.endpoint = endpoint;
|
|
785
|
+
this.reason = reason;
|
|
753
786
|
}
|
|
754
787
|
};
|
|
755
788
|
function resolveBoundaries(input) {
|
|
@@ -820,7 +853,7 @@ function resolveAnchorIndex(boundary, state, indexByMessageId, endpoint) {
|
|
|
820
853
|
throw new BoundaryNotFoundError(
|
|
821
854
|
"unknown",
|
|
822
855
|
endpoint,
|
|
823
|
-
`${label}="${boundary.raw}" does not exist in this session (typo or
|
|
856
|
+
`${label}="${boundary.raw}" does not exist in this session (typo, wrong session, or the message was edited/removed and its ref released) \u2014 run acp_status for current refs.`
|
|
824
857
|
);
|
|
825
858
|
}
|
|
826
859
|
const index = indexByMessageId.get(rawId);
|
|
@@ -834,10 +867,33 @@ function resolveAnchorIndex(boundary, state, indexByMessageId, endpoint) {
|
|
|
834
867
|
snapped: `${label}="${boundary.raw}" refers to a message already compressed into an active block \u2014 anchored to the active block covering it instead.`
|
|
835
868
|
};
|
|
836
869
|
}
|
|
870
|
+
const coveringActive = state.blocks.filter(
|
|
871
|
+
(block2) => block2.active && block2.effectiveMessageIds.includes(rawId)
|
|
872
|
+
);
|
|
873
|
+
if (coveringActive.length > 0) {
|
|
874
|
+
throw new BoundaryNotFoundError(
|
|
875
|
+
"consumed",
|
|
876
|
+
endpoint,
|
|
877
|
+
`${label}="${boundary.raw}" is already compressed into active block(s) ${coveringActive.map((block2) => block2.blockId).join(", ")} \u2014 retry with the block ref(s) instead of the message ref.`,
|
|
878
|
+
"consumed"
|
|
879
|
+
);
|
|
880
|
+
}
|
|
881
|
+
const generationReplaced = state.blocks.some(
|
|
882
|
+
(block2) => !block2.active && block2.effectiveMessageIds.includes(rawId)
|
|
883
|
+
);
|
|
884
|
+
if (generationReplaced) {
|
|
885
|
+
throw new BoundaryNotFoundError(
|
|
886
|
+
"consumed",
|
|
887
|
+
endpoint,
|
|
888
|
+
`${label}="${boundary.raw}" was compressed into a block that has since been distilled or consumed \u2014 its content is no longer available in this session; run acp_status for current refs.`,
|
|
889
|
+
"generation-replaced"
|
|
890
|
+
);
|
|
891
|
+
}
|
|
837
892
|
throw new BoundaryNotFoundError(
|
|
838
893
|
"consumed",
|
|
839
894
|
endpoint,
|
|
840
|
-
`${label}="${boundary.raw}"
|
|
895
|
+
`${label}="${boundary.raw}" no longer exists \u2014 the message was edited or removed (its content id changed); run acp_status for current refs.`,
|
|
896
|
+
"edit-drift"
|
|
841
897
|
);
|
|
842
898
|
}
|
|
843
899
|
const block = blockById(state, `b${boundary.numericId}`);
|
|
@@ -866,16 +922,21 @@ function resolveAnchorIndex(boundary, state, indexByMessageId, endpoint) {
|
|
|
866
922
|
};
|
|
867
923
|
}
|
|
868
924
|
if (!block.active) {
|
|
925
|
+
const consumer = state.blocks.find(
|
|
926
|
+
(candidate) => candidate.active && candidate.directBlockIds.includes(block.blockId) && visibleBlockAnchor(candidate, indexByMessageId) !== null
|
|
927
|
+
);
|
|
869
928
|
throw new BoundaryNotFoundError(
|
|
870
929
|
"consumed",
|
|
871
930
|
endpoint,
|
|
872
|
-
`${label}="b${boundary.numericId}"
|
|
931
|
+
consumer ? `${label}="b${boundary.numericId}" was consumed by ${consumer.blockId} (higher-tier block) \u2014 retry with ${consumer.blockId} instead.` : `${label}="b${boundary.numericId}" is inactive (distilled or garbage-collected) and none of its content is visible in the current context \u2014 run acp_status for current refs.`,
|
|
932
|
+
"generation-replaced"
|
|
873
933
|
);
|
|
874
934
|
}
|
|
875
935
|
throw new BoundaryNotFoundError(
|
|
876
936
|
"consumed",
|
|
877
937
|
endpoint,
|
|
878
|
-
`${label}="b${boundary.numericId}" is an active block but none of its content (raw messages or rendered summary) is
|
|
938
|
+
`${label}="b${boundary.numericId}" is an active block but none of its content (raw messages or rendered summary) is present in the current view \u2014 its messages were likely edited or removed (id drift); it will be deactivated on the next state sync. Run acp_status to verify.`,
|
|
939
|
+
"edit-drift"
|
|
879
940
|
);
|
|
880
941
|
}
|
|
881
942
|
function activeOwnerAnchor(state, ownedIds, indexByMessageId) {
|
|
@@ -1800,10 +1861,33 @@ function numericBlockId(id) {
|
|
|
1800
1861
|
const parsed = /^b(\d+)$/.exec(id);
|
|
1801
1862
|
return parsed ? Number(parsed[1]) : 0;
|
|
1802
1863
|
}
|
|
1864
|
+
function refGateDiagnostics(state, requestedRanges, unknownCount) {
|
|
1865
|
+
const highest = highestUsedIndex(state.messageRefs);
|
|
1866
|
+
const highestRef = highest > 0 ? indexToRef(highest) : "none";
|
|
1867
|
+
return `[diagnostics: session highest ref=${highestRef}, unknown ranges in request=${unknownCount}/${requestedRanges}, session history=${state.stats.compressionCount} compression(s), ${state.blocks.length} block(s)]`;
|
|
1868
|
+
}
|
|
1869
|
+
function danglingMessageRefs(state, messages, spec) {
|
|
1870
|
+
const visible = new Set(messages.map((m) => m.id));
|
|
1871
|
+
const dangling = [];
|
|
1872
|
+
for (const ref of [spec.startRef, spec.endRef]) {
|
|
1873
|
+
const parsed = parseBoundary(ref);
|
|
1874
|
+
if (!parsed || parsed.kind !== "message") continue;
|
|
1875
|
+
const rawId = state.messageRefs.byRef[parsed.raw] ?? state.messageRefs.byRef[indexToRef(parsed.numericId)];
|
|
1876
|
+
if (!rawId || visible.has(rawId)) continue;
|
|
1877
|
+
const covered = state.blocks.some(
|
|
1878
|
+
(block) => block.active && block.effectiveMessageIds.includes(rawId)
|
|
1879
|
+
);
|
|
1880
|
+
if (!covered) dangling.push(parsed.raw);
|
|
1881
|
+
}
|
|
1882
|
+
return dangling;
|
|
1883
|
+
}
|
|
1803
1884
|
function createCore(ports = {}) {
|
|
1804
1885
|
const countTokens = ports.countTokens ?? defaultCountTokens;
|
|
1805
1886
|
function applyCompression(input) {
|
|
1806
|
-
const state =
|
|
1887
|
+
const state = syncBlocks(
|
|
1888
|
+
input.messages,
|
|
1889
|
+
input.state
|
|
1890
|
+
).state;
|
|
1807
1891
|
const runId = allocateRunId(state);
|
|
1808
1892
|
let blocksCreated = 0;
|
|
1809
1893
|
let tokensCompressed = 0;
|
|
@@ -1900,7 +1984,15 @@ function createCore(ports = {}) {
|
|
|
1900
1984
|
if (!hasBlockBoundaryRange && totalRangeChars < input.config.compress.minCompressRange) {
|
|
1901
1985
|
const live = activeBlocks(state).map((b) => b.blockId).sort((x, y) => numericBlockId(x) - numericBlockId(y));
|
|
1902
1986
|
const liveHint = live.length > 0 ? ` Current active blocks span ${live[0]}..${live[live.length - 1]} \u2014 retry with startId/endId set to active block IDs in that span.` : "";
|
|
1903
|
-
const
|
|
1987
|
+
const diagnostics = refGateDiagnostics(
|
|
1988
|
+
state,
|
|
1989
|
+
input.ranges.length,
|
|
1990
|
+
unknownCount
|
|
1991
|
+
);
|
|
1992
|
+
const danglingRefs = consumedRanges.flatMap(
|
|
1993
|
+
(spec) => danglingMessageRefs(state, input.messages, spec)
|
|
1994
|
+
);
|
|
1995
|
+
const gateMessage = resolvableCount === 0 && consumedRanges.length === 0 && unknownCount > 0 ? `None of the ${input.ranges.length} requested range(s) resolved \u2014 every ref is unknown to this session. Refs are per-session snapshots, assigned once when a message is first rendered; no compress reassigns them, so unknown refs cannot come from an earlier compress in this session. They come from a different generation: a previous session instance (switching model or upstream mid-conversation starts a fresh session whose refs restart at m00001), the generation before a native-compaction rebase (which also resets refs to m00001), a typo, or a message that was edited or removed (its old ref was released when the content id changed). ${diagnostics} Run acp_status, then call the compress tool again using only the refs it reports.` : consumedRanges.length > 0 ? danglingRefs.length > 0 ? `Requested range(s) cannot be anchored (e.g. ${consumedRanges[0].startRef}..${consumedRanges[0].endRef}) \u2014 the refs exist in this session's ref map, but the messages they point to are no longer in the visible context and no active block covers them: the message content changed (or the message was filtered out of the view) and now carries a new ref, leaving your old refs dangling. ${diagnostics} Run acp_status, then call the compress tool again using only the refs it reports.` : `Requested range(s) already compressed (e.g. ${consumedRanges[0].startRef}..${consumedRanges[0].endRef}) \u2014 those refs no longer point to directly compressible content: the range is covered by active block(s) or the block ref(s) are stale (distilled or consumed). ${diagnostics} Run acp_status, then call the compress tool again using only the CURRENT compressible ranges it reports.${liveHint}` : `Total compressible content too small (${totalRangeChars} chars across ${countedRanges} range(s), min ${input.config.compress.minCompressRange}). Combine more messages into your range(s) to meet the threshold.`;
|
|
1904
1996
|
return {
|
|
1905
1997
|
state: input.state,
|
|
1906
1998
|
result: {
|
|
@@ -2043,9 +2135,15 @@ var assignRefsNode = {
|
|
|
2043
2135
|
run(io, ctx) {
|
|
2044
2136
|
const hasProtection = ctx.config.protectedTools.length > 0 || !!ctx.config.isToolProtected;
|
|
2045
2137
|
const protectedFn = hasProtection ? (m) => isMessageProtected(m, ctx.config) : void 0;
|
|
2138
|
+
const liveIds = new Set(io.messages.map((m) => m.id));
|
|
2139
|
+
for (const block of io.state.blocks) {
|
|
2140
|
+
if (!block.active) continue;
|
|
2141
|
+
for (const id of block.effectiveMessageIds) liveIds.add(id);
|
|
2142
|
+
}
|
|
2143
|
+
const pruned = pruneDeadRefs(io.state.messageRefs, liveIds);
|
|
2046
2144
|
const refResult = assignRefs(io.messages, {
|
|
2047
|
-
existing:
|
|
2048
|
-
nextIndex: highestUsedIndex(
|
|
2145
|
+
existing: pruned.map,
|
|
2146
|
+
nextIndex: highestUsedIndex(pruned.map) + 1,
|
|
2049
2147
|
isProtected: protectedFn
|
|
2050
2148
|
});
|
|
2051
2149
|
return { ...io, state: { ...io.state, messageRefs: refResult.map } };
|
|
@@ -2617,26 +2715,6 @@ function computeContextBreakdown(messages, total, growth, countTokens) {
|
|
|
2617
2715
|
}
|
|
2618
2716
|
return { system, tool, summaries, code, text, total, growth };
|
|
2619
2717
|
}
|
|
2620
|
-
function cloneState(state) {
|
|
2621
|
-
return {
|
|
2622
|
-
blocks: state.blocks.map((block) => ({
|
|
2623
|
-
...block,
|
|
2624
|
-
directMessageIds: [...block.directMessageIds],
|
|
2625
|
-
effectiveMessageIds: [...block.effectiveMessageIds],
|
|
2626
|
-
directBlockIds: [...block.directBlockIds]
|
|
2627
|
-
})),
|
|
2628
|
-
messageRefs: {
|
|
2629
|
-
byRaw: { ...state.messageRefs.byRaw },
|
|
2630
|
-
byRef: { ...state.messageRefs.byRef }
|
|
2631
|
-
},
|
|
2632
|
-
tokenSnapshot: { ...state.tokenSnapshot ?? {} },
|
|
2633
|
-
nudge: { ...state.nudge, anchors: { ...state.nudge.anchors } },
|
|
2634
|
-
stats: { ...state.stats },
|
|
2635
|
-
absorbed: (state.absorbed ?? []).map((record) => ({ ...record })),
|
|
2636
|
-
nextBlockId: state.nextBlockId,
|
|
2637
|
-
nextRunId: state.nextRunId
|
|
2638
|
-
};
|
|
2639
|
-
}
|
|
2640
2718
|
function scoreRelevance(block, terms) {
|
|
2641
2719
|
const topic = (block.topic ?? "").toLowerCase();
|
|
2642
2720
|
const summary = block.summary.toLowerCase();
|
|
@@ -2967,9 +3045,9 @@ function parseObjectValue(value, callId, diag) {
|
|
|
2967
3045
|
const content = value["content"];
|
|
2968
3046
|
if (content === void 0) {
|
|
2969
3047
|
const single = validateEntry(value, callId);
|
|
2970
|
-
if (single) {
|
|
3048
|
+
if ("range" in single) {
|
|
2971
3049
|
diag.kind = "ok";
|
|
2972
|
-
return finish([single], diag);
|
|
3050
|
+
return finish([single.range], diag);
|
|
2973
3051
|
}
|
|
2974
3052
|
diag.kind = "missing-content";
|
|
2975
3053
|
return finish([], diag);
|
|
@@ -2990,7 +3068,7 @@ function parseObjectValue(value, callId, diag) {
|
|
|
2990
3068
|
diag.kind = "content-not-array";
|
|
2991
3069
|
return finish([], diag);
|
|
2992
3070
|
}
|
|
2993
|
-
const { ranges, invalid } = validateEntries(entries, callId);
|
|
3071
|
+
const { ranges, invalid, reasons } = validateEntries(entries, callId);
|
|
2994
3072
|
const topTopic = stringOr(value["topic"]);
|
|
2995
3073
|
const topMaxChars = value["summaryMaxChars"];
|
|
2996
3074
|
const hasTopMaxChars = typeof topMaxChars === "number" && Number.isFinite(topMaxChars);
|
|
@@ -3001,6 +3079,7 @@ function parseObjectValue(value, callId, diag) {
|
|
|
3001
3079
|
}
|
|
3002
3080
|
}
|
|
3003
3081
|
diag.invalidItems = invalid;
|
|
3082
|
+
if (reasons.length > 0) diag.invalidReasons = reasons;
|
|
3004
3083
|
diag.kind = salvaged ? "truncated" : ranges.length > 0 ? "ok" : "no-valid-ranges";
|
|
3005
3084
|
return finish(ranges, diag);
|
|
3006
3085
|
}
|
|
@@ -3024,36 +3103,43 @@ function finish(ranges, diag) {
|
|
|
3024
3103
|
return { ranges, diagnostics: diag };
|
|
3025
3104
|
}
|
|
3026
3105
|
function finishSalvage(entries, callId, diag, truncatedShape) {
|
|
3027
|
-
const { ranges, invalid } = validateEntries(entries, callId);
|
|
3106
|
+
const { ranges, invalid, reasons } = validateEntries(entries, callId);
|
|
3028
3107
|
diag.invalidItems = invalid;
|
|
3108
|
+
if (reasons.length > 0) diag.invalidReasons = reasons;
|
|
3029
3109
|
diag.kind = entries.length > 0 || truncatedShape ? "truncated" : "malformed-json";
|
|
3030
3110
|
return finish(ranges, diag);
|
|
3031
3111
|
}
|
|
3032
3112
|
function validateEntries(entries, callId) {
|
|
3033
3113
|
const ranges = [];
|
|
3114
|
+
const reasons = [];
|
|
3034
3115
|
let invalid = 0;
|
|
3035
|
-
for (
|
|
3036
|
-
const
|
|
3037
|
-
if (range
|
|
3038
|
-
else
|
|
3116
|
+
for (let i = 0; i < entries.length; i++) {
|
|
3117
|
+
const outcome = validateEntry(entries[i], callId);
|
|
3118
|
+
if ("range" in outcome) ranges.push(outcome.range);
|
|
3119
|
+
else {
|
|
3120
|
+
invalid++;
|
|
3121
|
+
reasons.push(`entry ${i}: ${outcome.reason}`);
|
|
3122
|
+
}
|
|
3039
3123
|
}
|
|
3040
|
-
return { ranges, invalid };
|
|
3124
|
+
return { ranges, invalid, reasons };
|
|
3041
3125
|
}
|
|
3042
3126
|
function validateEntry(entry, callId) {
|
|
3043
|
-
if (entry === null || typeof entry !== "object" || Array.isArray(entry)) return
|
|
3127
|
+
if (entry === null || typeof entry !== "object" || Array.isArray(entry)) return { reason: "not an object" };
|
|
3044
3128
|
const e = entry;
|
|
3045
3129
|
const start = stringOr(e["startRef"]) ?? stringOr(e["startId"]) ?? stringOr(e["messageId"]);
|
|
3046
3130
|
const end = stringOr(e["endRef"]) ?? stringOr(e["endId"]) ?? stringOr(e["messageId"]);
|
|
3047
|
-
if (start === void 0 || end === void 0)
|
|
3131
|
+
if (start === void 0 || end === void 0) {
|
|
3132
|
+
return { reason: "missing range bounds (need startRef/startId and endRef/endId)" };
|
|
3133
|
+
}
|
|
3048
3134
|
const summary = stringOr(e["summary"]);
|
|
3049
|
-
if (summary === void 0) return
|
|
3135
|
+
if (summary === void 0) return { reason: "missing summary" };
|
|
3050
3136
|
const range = { startRef: start, endRef: end, summary };
|
|
3051
3137
|
const topic = stringOr(e["topic"]);
|
|
3052
3138
|
if (topic !== void 0) range.topic = topic;
|
|
3053
3139
|
const maxChars = e["summaryMaxChars"];
|
|
3054
3140
|
if (typeof maxChars === "number" && Number.isFinite(maxChars)) range.summaryMaxChars = maxChars;
|
|
3055
3141
|
if (callId !== void 0) range.compressCallId = callId;
|
|
3056
|
-
return range;
|
|
3142
|
+
return { range };
|
|
3057
3143
|
}
|
|
3058
3144
|
function stringOr(value) {
|
|
3059
3145
|
return typeof value === "string" && value.length > 0 ? value : void 0;
|
|
@@ -4670,7 +4756,12 @@ function createRuntime(adapter) {
|
|
|
4670
4756
|
const sm = ctx.sessionManager;
|
|
4671
4757
|
await store.save(state, sm.getSessionFile() ?? void 0, sm.getSessionId());
|
|
4672
4758
|
}
|
|
4673
|
-
|
|
4759
|
+
let refused = false;
|
|
4760
|
+
return { core, store, get refused() {
|
|
4761
|
+
return refused;
|
|
4762
|
+
}, set refused(v) {
|
|
4763
|
+
refused = v;
|
|
4764
|
+
}, get adapter() {
|
|
4674
4765
|
return adapterRef;
|
|
4675
4766
|
}, setAdapter: (a) => {
|
|
4676
4767
|
adapterRef = a;
|
|
@@ -9139,6 +9230,19 @@ function getSystemPromptText(ctx) {
|
|
|
9139
9230
|
return normalizeSystemPrompt(result);
|
|
9140
9231
|
}
|
|
9141
9232
|
|
|
9233
|
+
// src/omp.ts
|
|
9234
|
+
function isOmpHost(sm) {
|
|
9235
|
+
return !isPiHost(sm);
|
|
9236
|
+
}
|
|
9237
|
+
var OMP_UNSUPPORTED_MESSAGE = [
|
|
9238
|
+
"[billion-context-pi] This host is OMP (oh-my-pi), which is NOT supported \u2014 ACP has been disabled for this session.",
|
|
9239
|
+
`The in-process compression path is unreliable on OMP: the nudge's example refs diverge from the session's real refs, so compress calls fail with "does not exist in this session".`,
|
|
9240
|
+
"Use the billion-context proxy instead \u2014 it runs compression server-side and works on OMP:",
|
|
9241
|
+
" npm install -g billion-context",
|
|
9242
|
+
" bili omp",
|
|
9243
|
+
"Docs: https://github.com/ranxianglei/billion-context"
|
|
9244
|
+
].join("\n");
|
|
9245
|
+
|
|
9142
9246
|
// src/compress-tool.ts
|
|
9143
9247
|
function formatK2(n) {
|
|
9144
9248
|
return n >= 1e3 ? `${(n / 1e3).toFixed(1)}K` : String(n);
|
|
@@ -9177,6 +9281,7 @@ function makeCompressTool(runtime) {
|
|
|
9177
9281
|
],
|
|
9178
9282
|
parameters: CompressParams,
|
|
9179
9283
|
async execute(toolCallId, params, _signal, _onUpdate, ctx) {
|
|
9284
|
+
if (runtime.refused) return { details: void 0, content: [{ type: "text", text: OMP_UNSUPPORTED_MESSAGE }] };
|
|
9180
9285
|
let result;
|
|
9181
9286
|
try {
|
|
9182
9287
|
result = await handleCompress(params, runtime, ctx, toolCallId);
|
|
@@ -9358,6 +9463,7 @@ function makeDecompressTool(runtime) {
|
|
|
9358
9463
|
],
|
|
9359
9464
|
parameters: DecompressParams,
|
|
9360
9465
|
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
|
|
9466
|
+
if (runtime.refused) return { details: void 0, content: [{ type: "text", text: OMP_UNSUPPORTED_MESSAGE }] };
|
|
9361
9467
|
let result;
|
|
9362
9468
|
try {
|
|
9363
9469
|
result = await handleDecompress(params, runtime, ctx);
|
|
@@ -9597,6 +9703,7 @@ function makeSearchTool(runtime) {
|
|
|
9597
9703
|
],
|
|
9598
9704
|
parameters: SearchParams,
|
|
9599
9705
|
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
|
|
9706
|
+
if (runtime.refused) return { details: void 0, content: [{ type: "text", text: OMP_UNSUPPORTED_MESSAGE }] };
|
|
9600
9707
|
let result;
|
|
9601
9708
|
try {
|
|
9602
9709
|
result = await handleSearch(params, runtime, ctx);
|
|
@@ -10691,7 +10798,7 @@ async function runDelegate(pi, args, ctx, signal) {
|
|
|
10691
10798
|
await appendFile(file2, fallback ? `${fallback}
|
|
10692
10799
|
` : "(no output)\n");
|
|
10693
10800
|
}
|
|
10694
|
-
const effectiveCode = code
|
|
10801
|
+
const effectiveCode = effectiveExitCode(code, output, stderrText);
|
|
10695
10802
|
run.result = { code, file: file2, body: body2 };
|
|
10696
10803
|
run.status = effectiveCode === 0 ? "completed" : "failed";
|
|
10697
10804
|
run.finishedAt = Date.now();
|
|
@@ -10709,7 +10816,7 @@ async function runDelegate(pi, args, ctx, signal) {
|
|
|
10709
10816
|
return;
|
|
10710
10817
|
}
|
|
10711
10818
|
const mode = delegateDisplayUsage;
|
|
10712
|
-
const injected = injectResult(pi, args.agent, runId, args.task, code, file2, run.timedOut, run.usage, mode, run.usageReported, run.status === "failed" ? body2 : void 0);
|
|
10819
|
+
const injected = injectResult(pi, args.agent, runId, args.task, run.status, code, file2, run.timedOut, run.usage, mode, run.usageReported, run.status === "failed" ? body2 : void 0);
|
|
10713
10820
|
if (run.usage && !run.usageReported && (mode === "separate" || injected)) {
|
|
10714
10821
|
run.usageReported = true;
|
|
10715
10822
|
}
|
|
@@ -10834,15 +10941,18 @@ function formatSyncResult(agent, runId, task, r, file) {
|
|
|
10834
10941
|
const body = r.timedOut ? "(timed out)" : r.stderr.trim() || "(no stderr)";
|
|
10835
10942
|
return formatPayload(header, file, task, body);
|
|
10836
10943
|
}
|
|
10837
|
-
function
|
|
10944
|
+
function effectiveExitCode(code, output, stderr) {
|
|
10945
|
+
return code ?? (output || stderr ? 0 : null);
|
|
10946
|
+
}
|
|
10947
|
+
function injectResult(pi, agent, runId, task, status, code, file, timedOut, usage, mode = "separate", usageAlreadyReported, body) {
|
|
10838
10948
|
const send = pi.sendUserMessage;
|
|
10839
10949
|
if (typeof send !== "function") {
|
|
10840
10950
|
debug.event("delegate-inject-skipped", { runId, reason: "sendUserMessage unavailable" });
|
|
10841
10951
|
logWarn("delegate", { event: "inject-skipped", runId, reason: "sendUserMessage unavailable" });
|
|
10842
10952
|
return false;
|
|
10843
10953
|
}
|
|
10844
|
-
const failed =
|
|
10845
|
-
const
|
|
10954
|
+
const failed = status === "failed";
|
|
10955
|
+
const statusLabel = failed ? "FAILED \u26A0\uFE0F" : "completed";
|
|
10846
10956
|
const remaining = Array.from(runs.values()).filter((r) => r.status === "running").length;
|
|
10847
10957
|
const remainingLine = remaining > 0 ? ` ${remaining} delegate${remaining === 1 ? " is" : "s are"} still running; keep doing other work and their notifications will arrive as they finish.` : " No delegates are currently running.";
|
|
10848
10958
|
const timeoutNote = timedOut ? ` (timed out: ${timedOut})` : "";
|
|
@@ -10877,7 +10987,7 @@ Tokens: ${totalUsage.input.toLocaleString()} in, ${totalUsage.output.toLocaleStr
|
|
|
10877
10987
|
if (lines.length) usageNote = ` Usage: ${lines.join(", ")}.`;
|
|
10878
10988
|
}
|
|
10879
10989
|
const closing = failed ? "This delegate did NOT complete its task \u2014 its result is missing from your work. Read the error excerpt (and the result file if present), then decide whether to re-dispatch the task before wrapping up. This is an automated system notification, NOT a user message." : "This is an automated system notification, NOT a user message. Read the result file if you need the details, then continue your original task; do not treat this as a new user request.";
|
|
10880
|
-
const header = `[acp_delegate ${
|
|
10990
|
+
const header = `[acp_delegate ${statusLabel}] **${agent}** (runId \`${runId}\`, exit ${code ?? "?"})${timeoutNote}${remainingLine}${usageNote} ${closing}`;
|
|
10881
10991
|
const { text: recoveryText, covered } = buildRecoveryNotice(Array.from(runs.values()), runId);
|
|
10882
10992
|
const text = formatPayload(header, file, task, failed ? body : void 0) + (recoveryText ? `
|
|
10883
10993
|
|
|
@@ -10902,6 +11012,7 @@ function notifyTerminalFailure(pi, run, body) {
|
|
|
10902
11012
|
run.agent,
|
|
10903
11013
|
run.runId,
|
|
10904
11014
|
run.task,
|
|
11015
|
+
run.status,
|
|
10905
11016
|
run.result?.code ?? null,
|
|
10906
11017
|
run.result?.file ?? "",
|
|
10907
11018
|
run.timedOut,
|
|
@@ -10976,6 +11087,7 @@ function makeStatusTool(runtime) {
|
|
|
10976
11087
|
],
|
|
10977
11088
|
parameters: StatusParams,
|
|
10978
11089
|
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
|
|
11090
|
+
if (runtime.refused) return { details: void 0, content: [{ type: "text", text: OMP_UNSUPPORTED_MESSAGE }] };
|
|
10979
11091
|
let result;
|
|
10980
11092
|
try {
|
|
10981
11093
|
result = await handleStatus(params, runtime, ctx);
|
|
@@ -11503,7 +11615,7 @@ async function statusReport(runtime, ctx) {
|
|
|
11503
11615
|
const coveredIds = collectCoveredMessageIds(state);
|
|
11504
11616
|
const sentTokens = estimateTokens(coreMessages, coveredIds, imageTokens) + systemPromptTokens;
|
|
11505
11617
|
const turn = runtime.core.processTurn({ messages: coreMessages, state, config, tokenCount: anchorStale ? sentTokens : Math.max(sentTokens, realUsage?.tokens ?? 0) });
|
|
11506
|
-
const versionStr = "0.1.
|
|
11618
|
+
const versionStr = "0.1.54" ? `billion-context-pi@${"0.1.54"}` : void 0;
|
|
11507
11619
|
let text = buildStatusPanel({
|
|
11508
11620
|
version: versionStr,
|
|
11509
11621
|
tokenCount: sessionTokens,
|
|
@@ -11866,7 +11978,7 @@ async function autoInstallLatest(latest, extDirOverride) {
|
|
|
11866
11978
|
"--no-fund",
|
|
11867
11979
|
"--no-save"
|
|
11868
11980
|
];
|
|
11869
|
-
const prevVersion = (await readPackageJson(join8(extDir, "package.json")))?.version ?? "0.1.
|
|
11981
|
+
const prevVersion = (await readPackageJson(join8(extDir, "package.json")))?.version ?? "0.1.54";
|
|
11870
11982
|
const { code, stderr } = await runNpmImpl(installArgs(latest), { cwd: npmDir, timeout: 6e4 });
|
|
11871
11983
|
if (code !== 0) {
|
|
11872
11984
|
logWarn("update", {
|
|
@@ -11879,7 +11991,7 @@ async function autoInstallLatest(latest, extDirOverride) {
|
|
|
11879
11991
|
}
|
|
11880
11992
|
const verify = await verifyInstall(npmDir, latest);
|
|
11881
11993
|
if (!verify.ok) {
|
|
11882
|
-
const rollbackTo = SEMVER_RE.test(prevVersion) ? prevVersion : "0.1.
|
|
11994
|
+
const rollbackTo = SEMVER_RE.test(prevVersion) ? prevVersion : "0.1.54";
|
|
11883
11995
|
logWarn("update", { event: "auto-install-verify-failed", latest, reason: verify.reason, rollbackTo });
|
|
11884
11996
|
const rb = await runNpmImpl(installArgs(rollbackTo), { cwd: npmDir, timeout: 6e4 });
|
|
11885
11997
|
logInfo("update", { event: "rollback", from: latest, to: rollbackTo, ok: rb.code === 0 });
|
|
@@ -11940,7 +12052,7 @@ async function checkForUpdate(autoUpdate, notify) {
|
|
|
11940
12052
|
const runtimeVersion = await getRuntimeVersion();
|
|
11941
12053
|
const latest = await fetchLatestVersion();
|
|
11942
12054
|
if (!latest) return;
|
|
11943
|
-
const current = runtimeVersion ?? "0.1.
|
|
12055
|
+
const current = runtimeVersion ?? "0.1.54";
|
|
11944
12056
|
const hasUpdate = isNewer(latest, current);
|
|
11945
12057
|
debug.event("update-check", {
|
|
11946
12058
|
current,
|
|
@@ -11986,7 +12098,7 @@ function createAcpExtension(adapter = {}) {
|
|
|
11986
12098
|
return;
|
|
11987
12099
|
}
|
|
11988
12100
|
const runtime = createRuntime(adapter);
|
|
11989
|
-
wireCompactionDisable(pi);
|
|
12101
|
+
wireCompactionDisable(pi, runtime);
|
|
11990
12102
|
wireSessionLifecycle(pi, runtime);
|
|
11991
12103
|
wireContextTransform(pi, runtime);
|
|
11992
12104
|
wireSystemPrompt(pi, runtime);
|
|
@@ -12003,11 +12115,26 @@ function createAcpExtension(adapter = {}) {
|
|
|
12003
12115
|
};
|
|
12004
12116
|
}
|
|
12005
12117
|
var index_default = createAcpExtension();
|
|
12006
|
-
function wireCompactionDisable(pi) {
|
|
12007
|
-
pi.on("session_before_compact", () =>
|
|
12118
|
+
function wireCompactionDisable(pi, runtime) {
|
|
12119
|
+
pi.on("session_before_compact", () => {
|
|
12120
|
+
if (runtime.refused) return;
|
|
12121
|
+
return { cancel: true };
|
|
12122
|
+
});
|
|
12008
12123
|
}
|
|
12009
12124
|
function wireSessionLifecycle(pi, runtime) {
|
|
12125
|
+
let ompWarned = false;
|
|
12010
12126
|
pi.on("session_start", async (_event, ctx) => {
|
|
12127
|
+
if (isOmpHost(ctx.sessionManager)) {
|
|
12128
|
+
runtime.refused = true;
|
|
12129
|
+
if (!ompWarned) {
|
|
12130
|
+
ompWarned = true;
|
|
12131
|
+
const sid2 = ctx.sessionManager.getSessionId();
|
|
12132
|
+
logWarn("host", { event: "omp-unsupported", sid: sid2, action: "refused" });
|
|
12133
|
+
if (ctx.hasUI) ctx.ui.notify(OMP_UNSUPPORTED_MESSAGE, "warning");
|
|
12134
|
+
else console.error(OMP_UNSUPPORTED_MESSAGE);
|
|
12135
|
+
}
|
|
12136
|
+
return;
|
|
12137
|
+
}
|
|
12011
12138
|
runtime.store.invalidate();
|
|
12012
12139
|
runtime.clearNudgeTracking();
|
|
12013
12140
|
runtime.throttleFor(ctx.sessionManager.getSessionId()).reset();
|
|
@@ -12016,7 +12143,7 @@ function wireSessionLifecycle(pi, runtime) {
|
|
|
12016
12143
|
setDelegateDisplayUsage("separate");
|
|
12017
12144
|
const sid = ctx.sessionManager.getSessionId();
|
|
12018
12145
|
const modelInfo = ctx.model;
|
|
12019
|
-
logInfo("session", { event: "start", sid, cwd: ctx.cwd, debug: runtime.adapter.debug ?? null, version: true ? "0.1.
|
|
12146
|
+
logInfo("session", { event: "start", sid, cwd: ctx.cwd, debug: runtime.adapter.debug ?? null, version: true ? "0.1.54" : null, model: modelInfo?.id ?? null, modelApi: modelInfo?.api ?? null, contextWindow: modelInfo?.contextWindow ?? null });
|
|
12020
12147
|
try {
|
|
12021
12148
|
await runtime.reloadConfig(ctx.cwd);
|
|
12022
12149
|
setDelegateDisplayUsage(resolveDelegate(runtime.adapter).displayUsage);
|
|
@@ -12047,6 +12174,7 @@ function wireSessionLifecycle(pi, runtime) {
|
|
|
12047
12174
|
}
|
|
12048
12175
|
function wireContextTransform(pi, runtime) {
|
|
12049
12176
|
pi.on("context", async (event, ctx) => {
|
|
12177
|
+
if (runtime.refused) return;
|
|
12050
12178
|
const sid = ctx.sessionManager.getSessionId();
|
|
12051
12179
|
const release = await runtime.acquireLock(sid);
|
|
12052
12180
|
try {
|
|
@@ -12185,6 +12313,7 @@ ${rendered.text}${example}`);
|
|
|
12185
12313
|
}
|
|
12186
12314
|
function wireSystemPrompt(pi, runtime) {
|
|
12187
12315
|
pi.on("before_agent_start", (event) => {
|
|
12316
|
+
if (runtime.refused) return;
|
|
12188
12317
|
const delegate = runtime.adapter.delegate !== false;
|
|
12189
12318
|
const acp = buildAcpSystemPrompt(runtime.prompts);
|
|
12190
12319
|
const prompt = delegate ? `${acp}
|