@skillstate/opencode 3.0.0 → 3.0.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 +196 -131
- package/dist/feedback.d.ts +178 -0
- package/dist/feedback.d.ts.map +1 -0
- package/dist/feedback.js +235 -0
- package/dist/feedback.js.map +1 -0
- package/dist/index.d.ts +33 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +27 -3
- package/dist/index.js.map +1 -1
- package/dist/mode.d.ts +97 -0
- package/dist/mode.d.ts.map +1 -0
- package/dist/mode.js +111 -0
- package/dist/mode.js.map +1 -0
- package/dist/opencode-adapter.d.ts +4 -3
- package/dist/opencode-adapter.d.ts.map +1 -1
- package/dist/opencode-adapter.js.map +1 -1
- package/dist/paper-mode.d.ts +421 -0
- package/dist/paper-mode.d.ts.map +1 -0
- package/dist/paper-mode.js +445 -0
- package/dist/paper-mode.js.map +1 -0
- package/dist/plugin.d.ts +158 -8
- package/dist/plugin.d.ts.map +1 -1
- package/dist/plugin.js +788 -21
- package/dist/plugin.js.map +1 -1
- package/dist/response-sink.d.ts +208 -0
- package/dist/response-sink.d.ts.map +1 -0
- package/dist/response-sink.js +243 -0
- package/dist/response-sink.js.map +1 -0
- package/dist/runtime.d.ts +203 -0
- package/dist/runtime.d.ts.map +1 -0
- package/dist/runtime.js +332 -0
- package/dist/runtime.js.map +1 -0
- package/dist/spec-loader.d.ts +81 -0
- package/dist/spec-loader.d.ts.map +1 -0
- package/dist/spec-loader.js +163 -0
- package/dist/spec-loader.js.map +1 -0
- package/dist/step-boundary.d.ts +91 -0
- package/dist/step-boundary.d.ts.map +1 -0
- package/dist/step-boundary.js +109 -0
- package/dist/step-boundary.js.map +1 -0
- package/dist/system-hint.d.ts +70 -3
- package/dist/system-hint.d.ts.map +1 -1
- package/dist/system-hint.js +90 -11
- package/dist/system-hint.js.map +1 -1
- package/dist/tools.d.ts +35 -1
- package/dist/tools.d.ts.map +1 -1
- package/dist/tools.js +16 -0
- package/dist/tools.js.map +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resolving P — the procedural specification the paper's prompt is built on.
|
|
3
|
+
*
|
|
4
|
+
* ── Why the plugin needs a spec at all ────────────────────────────────────
|
|
5
|
+
*
|
|
6
|
+
* A.4 is `Format(P, Σₜ, Oₜ)`, and P is the first argument. Paper mode
|
|
7
|
+
* therefore cannot assemble a paper-conformant prompt without one, even
|
|
8
|
+
* though the store ({@link ProjectStateStore}) is deliberately schema-free:
|
|
9
|
+
* notes mode never shows the model a schema, so it never needs P.
|
|
10
|
+
*
|
|
11
|
+
* ── Resolution order ─────────────────────────────────────────────────────
|
|
12
|
+
*
|
|
13
|
+
* 1. `<project>/skill-spec.json` — the file `skillstate init` writes and the
|
|
14
|
+
* one the CLI's `--spec` already points at (`config.ts` default
|
|
15
|
+
* `specPath: './skill-spec.json'`). A project that has customised its
|
|
16
|
+
* procedure gets that procedure.
|
|
17
|
+
* 2. {@link GENERIC_PROCEDURE_SPEC} — the built-in, domain-neutral default.
|
|
18
|
+
*
|
|
19
|
+
* ── A malformed spec file must not reach the model ────────────────────────
|
|
20
|
+
*
|
|
21
|
+
* A half-written or hand-mangled `skill-spec.json` is exactly the kind of
|
|
22
|
+
* input that produced the v1 failure, where the spec's own instructions
|
|
23
|
+
* ("You are an autonomous CTF agent … find the flag") overrode the user. So
|
|
24
|
+
* the file is VALIDATED before it is trusted, field by field, and anything
|
|
25
|
+
* that does not typecheck falls back to the built-in spec with the reason
|
|
26
|
+
* recorded. There is no code path that feeds an unvalidated P to the model.
|
|
27
|
+
*
|
|
28
|
+
* Reads are cached per directory for the lifetime of the resolver: the
|
|
29
|
+
* `context` hook runs on every model request, and re-reading and re-validating
|
|
30
|
+
* a file that cannot change without the user editing it is pure overhead on
|
|
31
|
+
* the agent loop's hot path. {@link SpecResolver.invalidate} drops the cache
|
|
32
|
+
* for callers that do edit it.
|
|
33
|
+
*/
|
|
34
|
+
import * as fs from 'node:fs';
|
|
35
|
+
import * as path from 'node:path';
|
|
36
|
+
import { GENERIC_PROCEDURE_SPEC, isPlainObject } from '@skillstate/core';
|
|
37
|
+
/** The file a project keeps its procedural spec in. */
|
|
38
|
+
export const SPEC_FILE_NAME = 'skill-spec.json';
|
|
39
|
+
const SCHEMA_TYPES = new Set([
|
|
40
|
+
'string',
|
|
41
|
+
'number',
|
|
42
|
+
'boolean',
|
|
43
|
+
'array',
|
|
44
|
+
'object',
|
|
45
|
+
]);
|
|
46
|
+
function validateSchema(value, at) {
|
|
47
|
+
if (!isPlainObject(value))
|
|
48
|
+
return `${at} is not an object`;
|
|
49
|
+
const schema = {};
|
|
50
|
+
for (const [key, raw] of Object.entries(value)) {
|
|
51
|
+
if (!isPlainObject(raw))
|
|
52
|
+
return `${at}.${key} is not an object`;
|
|
53
|
+
const type = raw['type'];
|
|
54
|
+
if (typeof type !== 'string' || !SCHEMA_TYPES.has(type)) {
|
|
55
|
+
return `${at}.${key}.type must be one of ${[...SCHEMA_TYPES].join(', ')}`;
|
|
56
|
+
}
|
|
57
|
+
if (!('default' in raw))
|
|
58
|
+
return `${at}.${key}.default is required`;
|
|
59
|
+
const description = raw['description'];
|
|
60
|
+
if (description !== undefined && typeof description !== 'string') {
|
|
61
|
+
return `${at}.${key}.description must be a string`;
|
|
62
|
+
}
|
|
63
|
+
const field = { type: type, default: raw['default'] };
|
|
64
|
+
if (typeof description === 'string')
|
|
65
|
+
field.description = description;
|
|
66
|
+
schema[key] = field;
|
|
67
|
+
}
|
|
68
|
+
return schema;
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Validate a parsed spec document. Returns the spec, or a sentence naming
|
|
72
|
+
* the first field that failed. Pure.
|
|
73
|
+
*
|
|
74
|
+
* The checks are deliberately structural only — they reject a document that
|
|
75
|
+
* could not be rendered or that would misdescribe Σₜ, and nothing subtler.
|
|
76
|
+
* Semantic review of an instructions string is not something a type check can
|
|
77
|
+
* do, and pretending otherwise would be worse than not checking.
|
|
78
|
+
*/
|
|
79
|
+
export function parseSpec(value) {
|
|
80
|
+
if (!isPlainObject(value))
|
|
81
|
+
return 'spec is not an object';
|
|
82
|
+
const id = value['id'];
|
|
83
|
+
if (typeof id !== 'string' || id.trim() === '')
|
|
84
|
+
return 'id must be a non-empty string';
|
|
85
|
+
const name = value['name'];
|
|
86
|
+
if (typeof name !== 'string' || name.trim() === '')
|
|
87
|
+
return 'name must be a non-empty string';
|
|
88
|
+
const version = value['version'];
|
|
89
|
+
if (typeof version !== 'string' || version.trim() === '') {
|
|
90
|
+
return 'version must be a non-empty string';
|
|
91
|
+
}
|
|
92
|
+
const instructions = value['instructions'];
|
|
93
|
+
if (typeof instructions !== 'string' || instructions.trim() === '') {
|
|
94
|
+
return 'instructions must be a non-empty string';
|
|
95
|
+
}
|
|
96
|
+
const schema = validateSchema(value['schema'], 'schema');
|
|
97
|
+
if (typeof schema === 'string')
|
|
98
|
+
return schema;
|
|
99
|
+
return { id, name, version, instructions, schema };
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Per-project spec resolution with a cache.
|
|
103
|
+
*
|
|
104
|
+
* One instance per plugin setup. The cache key is the resolved project
|
|
105
|
+
* directory, so a single OpenCode server serving several checkouts gets a
|
|
106
|
+
* different spec per checkout without the caller managing that.
|
|
107
|
+
*/
|
|
108
|
+
export class SpecResolver {
|
|
109
|
+
cache = new Map();
|
|
110
|
+
/**
|
|
111
|
+
* The spec for `directory`, reading and validating the file at most once
|
|
112
|
+
* per {@link invalidate}.
|
|
113
|
+
*/
|
|
114
|
+
resolve(directory) {
|
|
115
|
+
const key = path.resolve(directory);
|
|
116
|
+
const cached = this.cache.get(key);
|
|
117
|
+
if (cached !== undefined)
|
|
118
|
+
return cached;
|
|
119
|
+
const resolution = this.load(key);
|
|
120
|
+
this.cache.set(key, resolution);
|
|
121
|
+
return resolution;
|
|
122
|
+
}
|
|
123
|
+
/** Drop the cached spec for `directory` (or for every directory). */
|
|
124
|
+
invalidate(directory) {
|
|
125
|
+
if (directory === undefined)
|
|
126
|
+
this.cache.clear();
|
|
127
|
+
else
|
|
128
|
+
this.cache.delete(path.resolve(directory));
|
|
129
|
+
}
|
|
130
|
+
load(directory) {
|
|
131
|
+
const file = path.join(directory, SPEC_FILE_NAME);
|
|
132
|
+
let raw;
|
|
133
|
+
try {
|
|
134
|
+
raw = fs.readFileSync(file, 'utf-8');
|
|
135
|
+
}
|
|
136
|
+
catch {
|
|
137
|
+
return { spec: GENERIC_PROCEDURE_SPEC, source: 'builtin' };
|
|
138
|
+
}
|
|
139
|
+
let parsed;
|
|
140
|
+
try {
|
|
141
|
+
parsed = JSON.parse(raw);
|
|
142
|
+
}
|
|
143
|
+
catch (error) {
|
|
144
|
+
return {
|
|
145
|
+
spec: GENERIC_PROCEDURE_SPEC,
|
|
146
|
+
source: 'builtin',
|
|
147
|
+
path: file,
|
|
148
|
+
rejected: `invalid JSON: ${String(error)}`,
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
const validated = parseSpec(parsed);
|
|
152
|
+
if (typeof validated === 'string') {
|
|
153
|
+
return {
|
|
154
|
+
spec: GENERIC_PROCEDURE_SPEC,
|
|
155
|
+
source: 'builtin',
|
|
156
|
+
path: file,
|
|
157
|
+
rejected: validated,
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
return { spec: validated, source: 'file', path: file };
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
//# sourceMappingURL=spec-loader.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"spec-loader.js","sourceRoot":"","sources":["../src/spec-loader.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgCG;AAEH,OAAO,KAAK,EAAE,MAAM,SAAS,CAAC;AAC9B,OAAO,KAAK,IAAI,MAAM,WAAW,CAAC;AAClC,OAAO,EAAE,sBAAsB,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAC;AAGzE,uDAAuD;AACvD,MAAM,CAAC,MAAM,cAAc,GAAG,iBAAiB,CAAC;AAoBhD,MAAM,YAAY,GAAwB,IAAI,GAAG,CAAC;IAChD,QAAQ;IACR,QAAQ;IACR,SAAS;IACT,OAAO;IACP,QAAQ;CACT,CAAC,CAAC;AAEH,SAAS,cAAc,CAAC,KAAc,EAAE,EAAU;IAChD,IAAI,CAAC,aAAa,CAAC,KAAK,CAAC;QAAE,OAAO,GAAG,EAAE,mBAAmB,CAAC;IAC3D,MAAM,MAAM,GAAgB,EAAE,CAAC;IAC/B,KAAK,MAAM,CAAC,GAAG,EAAE,GAAG,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,KAAK,CAAC,EAAE,CAAC;QAC/C,IAAI,CAAC,aAAa,CAAC,GAAG,CAAC;YAAE,OAAO,GAAG,EAAE,IAAI,GAAG,mBAAmB,CAAC;QAChE,MAAM,IAAI,GAAG,GAAG,CAAC,MAAM,CAAC,CAAC;QACzB,IAAI,OAAO,IAAI,KAAK,QAAQ,IAAI,CAAC,YAAY,CAAC,GAAG,CAAC,IAAI,CAAC,EAAE,CAAC;YACxD,OAAO,GAAG,EAAE,IAAI,GAAG,wBAAwB,CAAC,GAAG,YAAY,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC;QAC5E,CAAC;QACD,IAAI,CAAC,CAAC,SAAS,IAAI,GAAG,CAAC;YAAE,OAAO,GAAG,EAAE,IAAI,GAAG,sBAAsB,CAAC;QACnE,MAAM,WAAW,GAAG,GAAG,CAAC,aAAa,CAAC,CAAC;QACvC,IAAI,WAAW,KAAK,SAAS,IAAI,OAAO,WAAW,KAAK,QAAQ,EAAE,CAAC;YACjE,OAAO,GAAG,EAAE,IAAI,GAAG,+BAA+B,CAAC;QACrD,CAAC;QACD,MAAM,KAAK,GAAgB,EAAE,IAAI,EAAE,IAA2B,EAAE,OAAO,EAAE,GAAG,CAAC,SAAS,CAAC,EAAE,CAAC;QAC1F,IAAI,OAAO,WAAW,KAAK,QAAQ;YAAE,KAAK,CAAC,WAAW,GAAG,WAAW,CAAC;QACrE,MAAM,CAAC,GAAG,CAAC,GAAG,KAAK,CAAC;IACtB,CAAC;IACD,OAAO,MAAM,CAAC;AAChB,CAAC;AAED;;;;;;;;GAQG;AACH,MAAM,UAAU,SAAS,CAAC,KAAc;IACtC,IAAI,CAAC,aAAa,CAAC,KAAK,CAAC;QAAE,OAAO,uBAAuB,CAAC;IAC1D,MAAM,EAAE,GAAG,KAAK,CAAC,IAAI,CAAC,CAAC;IACvB,IAAI,OAAO,EAAE,KAAK,QAAQ,IAAI,EAAE,CAAC,IAAI,EAAE,KAAK,EAAE;QAAE,OAAO,+BAA+B,CAAC;IACvF,MAAM,IAAI,GAAG,KAAK,CAAC,MAAM,CAAC,CAAC;IAC3B,IAAI,OAAO,IAAI,KAAK,QAAQ,IAAI,IAAI,CAAC,IAAI,EAAE,KAAK,EAAE;QAAE,OAAO,iCAAiC,CAAC;IAC7F,MAAM,OAAO,GAAG,KAAK,CAAC,SAAS,CAAC,CAAC;IACjC,IAAI,OAAO,OAAO,KAAK,QAAQ,IAAI,OAAO,CAAC,IAAI,EAAE,KAAK,EAAE,EAAE,CAAC;QACzD,OAAO,oCAAoC,CAAC;IAC9C,CAAC;IACD,MAAM,YAAY,GAAG,KAAK,CAAC,cAAc,CAAC,CAAC;IAC3C,IAAI,OAAO,YAAY,KAAK,QAAQ,IAAI,YAAY,CAAC,IAAI,EAAE,KAAK,EAAE,EAAE,CAAC;QACnE,OAAO,yCAAyC,CAAC;IACnD,CAAC;IACD,MAAM,MAAM,GAAG,cAAc,CAAC,KAAK,CAAC,QAAQ,CAAC,EAAE,QAAQ,CAAC,CAAC;IACzD,IAAI,OAAO,MAAM,KAAK,QAAQ;QAAE,OAAO,MAAM,CAAC;IAC9C,OAAO,EAAE,EAAE,EAAE,IAAI,EAAE,OAAO,EAAE,YAAY,EAAE,MAAM,EAAE,CAAC;AACrD,CAAC;AAED;;;;;;GAMG;AACH,MAAM,OAAO,YAAY;IACN,KAAK,GAAG,IAAI,GAAG,EAA0B,CAAC;IAE3D;;;OAGG;IACH,OAAO,CAAC,SAAiB;QACvB,MAAM,GAAG,GAAG,IAAI,CAAC,OAAO,CAAC,SAAS,CAAC,CAAC;QACpC,MAAM,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;QACnC,IAAI,MAAM,KAAK,SAAS;YAAE,OAAO,MAAM,CAAC;QACxC,MAAM,UAAU,GAAG,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;QAClC,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,GAAG,EAAE,UAAU,CAAC,CAAC;QAChC,OAAO,UAAU,CAAC;IACpB,CAAC;IAED,qEAAqE;IACrE,UAAU,CAAC,SAAkB;QAC3B,IAAI,SAAS,KAAK,SAAS;YAAE,IAAI,CAAC,KAAK,CAAC,KAAK,EAAE,CAAC;;YAC3C,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,IAAI,CAAC,OAAO,CAAC,SAAS,CAAC,CAAC,CAAC;IAClD,CAAC;IAEO,IAAI,CAAC,SAAiB;QAC5B,MAAM,IAAI,GAAG,IAAI,CAAC,IAAI,CAAC,SAAS,EAAE,cAAc,CAAC,CAAC;QAClD,IAAI,GAAW,CAAC;QAChB,IAAI,CAAC;YACH,GAAG,GAAG,EAAE,CAAC,YAAY,CAAC,IAAI,EAAE,OAAO,CAAC,CAAC;QACvC,CAAC;QAAC,MAAM,CAAC;YACP,OAAO,EAAE,IAAI,EAAE,sBAAsB,EAAE,MAAM,EAAE,SAAS,EAAE,CAAC;QAC7D,CAAC;QACD,IAAI,MAAe,CAAC;QACpB,IAAI,CAAC;YACH,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;QAC3B,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,OAAO;gBACL,IAAI,EAAE,sBAAsB;gBAC5B,MAAM,EAAE,SAAS;gBACjB,IAAI,EAAE,IAAI;gBACV,QAAQ,EAAE,iBAAiB,MAAM,CAAC,KAAK,CAAC,EAAE;aAC3C,CAAC;QACJ,CAAC;QACD,MAAM,SAAS,GAAG,SAAS,CAAC,MAAM,CAAC,CAAC;QACpC,IAAI,OAAO,SAAS,KAAK,QAAQ,EAAE,CAAC;YAClC,OAAO;gBACL,IAAI,EAAE,sBAAsB;gBAC5B,MAAM,EAAE,SAAS;gBACjB,IAAI,EAAE,IAAI;gBACV,QAAQ,EAAE,SAAS;aACpB,CAAC;QACJ,CAAC;QACD,OAAO,EAAE,IAAI,EAAE,SAAS,EAAE,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC;IACzD,CAAC;CACF"}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The step boundary — §5.1's one observation per step, enforced in code.
|
|
3
|
+
*
|
|
4
|
+
* ── The divergence this fixes ─────────────────────────────────────────────
|
|
5
|
+
*
|
|
6
|
+
* The paper's Algorithm 1 alternates, once per step:
|
|
7
|
+
*
|
|
8
|
+
* Aₜ ← Format(P, Σₜ, Oₜ); resp ← llm(Aₜ); Σₜ₊₁ ← Σₜ ⊕ ΔΣₜ; Oₜ₊₁ ← execute(aₜ)
|
|
9
|
+
*
|
|
10
|
+
* One prompt, one patch, one action, one observation. The model is never
|
|
11
|
+
* given a loop of its own, so it has no opportunity to run twenty things
|
|
12
|
+
* inside one step, and the state cannot lag the work.
|
|
13
|
+
*
|
|
14
|
+
* This integration cannot own the model call or the tool execution — the
|
|
15
|
+
* OpenCode plugin API exposes neither, and that is a real limit rather than a
|
|
16
|
+
* design choice. What it can do is own the *boundary*, and that is the half
|
|
17
|
+
* that was missing. Delegating execution to the host's agent loop handed the
|
|
18
|
+
* model an unbounded inner loop, and it used it: 21 tool calls, 3 text
|
|
19
|
+
* blocks, one state patch written at the end from whatever observation was
|
|
20
|
+
* current. Two models, repeated runs. The state lagged the work, and a model
|
|
21
|
+
* whose running total lags cannot accumulate.
|
|
22
|
+
*
|
|
23
|
+
* The fix uses a capability that is present and unused: `SessionContext.tools`
|
|
24
|
+
* is handed to the `context` hook on *every* model request. So requests
|
|
25
|
+
* alternate. One may act — tools present, the host executes, the observation
|
|
26
|
+
* lands in Oₜ. The next is given no tools at all, and a model that has just
|
|
27
|
+
* acted and is asked again with nothing to call can only answer in text, which
|
|
28
|
+
* is exactly where `state_patch` lives.
|
|
29
|
+
*
|
|
30
|
+
* That is §5.1's alternation with the host standing in for both `llm` and
|
|
31
|
+
* `execute`. It is not a simulation of the loop: the host really runs the
|
|
32
|
+
* action, and the state really moves between steps.
|
|
33
|
+
*
|
|
34
|
+
* ── What it does not do ───────────────────────────────────────────────────
|
|
35
|
+
*
|
|
36
|
+
* It cannot make the model write a *good* patch, only make it answer. The
|
|
37
|
+
* merge, the validation and the retry-with-rollback are unchanged, and a
|
|
38
|
+
* patch that fails validation is still refused. This buys the paper's
|
|
39
|
+
* one-observation-per-step; it does not buy correctness of reasoning.
|
|
40
|
+
*/
|
|
41
|
+
export declare class StepBoundary {
|
|
42
|
+
#private;
|
|
43
|
+
/**
|
|
44
|
+
* Decide what a request may do, and remember the answer.
|
|
45
|
+
*
|
|
46
|
+
* Returns true when the model may act. A session starts in `act`, so the
|
|
47
|
+
* first request can do real work; every request after an action is a report.
|
|
48
|
+
*/
|
|
49
|
+
mayAct(sessionID: string): boolean;
|
|
50
|
+
/**
|
|
51
|
+
* Record that an action ran, so the next request must report.
|
|
52
|
+
*
|
|
53
|
+
* Called when the host executed a tool on this session's behalf. There is
|
|
54
|
+
* no event that says "a tool finished" in a form the plugin can trust for
|
|
55
|
+
* this, so the boundary is advanced from the patch instead — see
|
|
56
|
+
* {@link reportRequired}. Kept as a separate method so the trigger can
|
|
57
|
+
* change without the cycle changing.
|
|
58
|
+
*/
|
|
59
|
+
actionTaken(sessionID: string): void;
|
|
60
|
+
/**
|
|
61
|
+
* Whether this request must be answered with a state patch.
|
|
62
|
+
*
|
|
63
|
+
* The single question the context hook needs. It is also the whole point:
|
|
64
|
+
* the model gets one turn to act and one turn to account for it.
|
|
65
|
+
*/
|
|
66
|
+
reportRequired(sessionID: string): boolean;
|
|
67
|
+
/**
|
|
68
|
+
* A patch was applied, so the next request may act again.
|
|
69
|
+
*
|
|
70
|
+
* Only an *applied* patch clears the phase. A rejected one leaves the model
|
|
71
|
+
* in `report`, which is what the retry-with-rollback in §6.3 needs: it is
|
|
72
|
+
* re-asked for the same step rather than being let off the hook to act
|
|
73
|
+
* before it has recorded anything.
|
|
74
|
+
*/
|
|
75
|
+
patchApplied(sessionID: string): void;
|
|
76
|
+
/** Reset a session, on teardown or a fresh run. */
|
|
77
|
+
reset(sessionID: string): void;
|
|
78
|
+
/** Forget everything. A plugin unload must not leak a phase map. */
|
|
79
|
+
clear(): void;
|
|
80
|
+
/** How many sessions are mid-cycle, for diagnostics and tests. */
|
|
81
|
+
get size(): number;
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Whether an action ends the procedure.
|
|
85
|
+
*
|
|
86
|
+
* Shared with the runtime so the two agree on what "finished" means. They
|
|
87
|
+
* answer the same question from the same string, and two copies of that would
|
|
88
|
+
* be two definitions to keep in step.
|
|
89
|
+
*/
|
|
90
|
+
export declare function isTerminalAction(action: string): boolean;
|
|
91
|
+
//# sourceMappingURL=step-boundary.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"step-boundary.d.ts","sourceRoot":"","sources":["../src/step-boundary.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAuCG;AAQH,qBAAa,YAAY;;IAGvB;;;;;OAKG;IACH,MAAM,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAEjC;IAED;;;;;;;;OAQG;IACH,WAAW,CAAC,SAAS,EAAE,MAAM,GAAG,IAAI,CAEnC;IAED;;;;;OAKG;IACH,cAAc,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAEzC;IAED;;;;;;;OAOG;IACH,YAAY,CAAC,SAAS,EAAE,MAAM,GAAG,IAAI,CAEpC;IAED,mDAAmD;IACnD,KAAK,CAAC,SAAS,EAAE,MAAM,GAAG,IAAI,CAE7B;IAED,oEAAoE;IACpE,KAAK,IAAI,IAAI,CAEZ;IAED,kEAAkE;IAClE,IAAI,IAAI,IAAI,MAAM,CAEjB;CACF;AAED;;;;;;GAMG;AACH,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAExD"}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The step boundary — §5.1's one observation per step, enforced in code.
|
|
3
|
+
*
|
|
4
|
+
* ── The divergence this fixes ─────────────────────────────────────────────
|
|
5
|
+
*
|
|
6
|
+
* The paper's Algorithm 1 alternates, once per step:
|
|
7
|
+
*
|
|
8
|
+
* Aₜ ← Format(P, Σₜ, Oₜ); resp ← llm(Aₜ); Σₜ₊₁ ← Σₜ ⊕ ΔΣₜ; Oₜ₊₁ ← execute(aₜ)
|
|
9
|
+
*
|
|
10
|
+
* One prompt, one patch, one action, one observation. The model is never
|
|
11
|
+
* given a loop of its own, so it has no opportunity to run twenty things
|
|
12
|
+
* inside one step, and the state cannot lag the work.
|
|
13
|
+
*
|
|
14
|
+
* This integration cannot own the model call or the tool execution — the
|
|
15
|
+
* OpenCode plugin API exposes neither, and that is a real limit rather than a
|
|
16
|
+
* design choice. What it can do is own the *boundary*, and that is the half
|
|
17
|
+
* that was missing. Delegating execution to the host's agent loop handed the
|
|
18
|
+
* model an unbounded inner loop, and it used it: 21 tool calls, 3 text
|
|
19
|
+
* blocks, one state patch written at the end from whatever observation was
|
|
20
|
+
* current. Two models, repeated runs. The state lagged the work, and a model
|
|
21
|
+
* whose running total lags cannot accumulate.
|
|
22
|
+
*
|
|
23
|
+
* The fix uses a capability that is present and unused: `SessionContext.tools`
|
|
24
|
+
* is handed to the `context` hook on *every* model request. So requests
|
|
25
|
+
* alternate. One may act — tools present, the host executes, the observation
|
|
26
|
+
* lands in Oₜ. The next is given no tools at all, and a model that has just
|
|
27
|
+
* acted and is asked again with nothing to call can only answer in text, which
|
|
28
|
+
* is exactly where `state_patch` lives.
|
|
29
|
+
*
|
|
30
|
+
* That is §5.1's alternation with the host standing in for both `llm` and
|
|
31
|
+
* `execute`. It is not a simulation of the loop: the host really runs the
|
|
32
|
+
* action, and the state really moves between steps.
|
|
33
|
+
*
|
|
34
|
+
* ── What it does not do ───────────────────────────────────────────────────
|
|
35
|
+
*
|
|
36
|
+
* It cannot make the model write a *good* patch, only make it answer. The
|
|
37
|
+
* merge, the validation and the retry-with-rollback are unchanged, and a
|
|
38
|
+
* patch that fails validation is still refused. This buys the paper's
|
|
39
|
+
* one-observation-per-step; it does not buy correctness of reasoning.
|
|
40
|
+
*/
|
|
41
|
+
/** Actions that end the procedure rather than continuing it. */
|
|
42
|
+
const TERMINAL = new Set(['done', 'complete', 'completed', 'finished', 'stop', 'end', '']);
|
|
43
|
+
export class StepBoundary {
|
|
44
|
+
#phase = new Map();
|
|
45
|
+
/**
|
|
46
|
+
* Decide what a request may do, and remember the answer.
|
|
47
|
+
*
|
|
48
|
+
* Returns true when the model may act. A session starts in `act`, so the
|
|
49
|
+
* first request can do real work; every request after an action is a report.
|
|
50
|
+
*/
|
|
51
|
+
mayAct(sessionID) {
|
|
52
|
+
return (this.#phase.get(sessionID) ?? 'act') === 'act';
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Record that an action ran, so the next request must report.
|
|
56
|
+
*
|
|
57
|
+
* Called when the host executed a tool on this session's behalf. There is
|
|
58
|
+
* no event that says "a tool finished" in a form the plugin can trust for
|
|
59
|
+
* this, so the boundary is advanced from the patch instead — see
|
|
60
|
+
* {@link reportRequired}. Kept as a separate method so the trigger can
|
|
61
|
+
* change without the cycle changing.
|
|
62
|
+
*/
|
|
63
|
+
actionTaken(sessionID) {
|
|
64
|
+
this.#phase.set(sessionID, 'report');
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Whether this request must be answered with a state patch.
|
|
68
|
+
*
|
|
69
|
+
* The single question the context hook needs. It is also the whole point:
|
|
70
|
+
* the model gets one turn to act and one turn to account for it.
|
|
71
|
+
*/
|
|
72
|
+
reportRequired(sessionID) {
|
|
73
|
+
return (this.#phase.get(sessionID) ?? 'act') === 'report';
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* A patch was applied, so the next request may act again.
|
|
77
|
+
*
|
|
78
|
+
* Only an *applied* patch clears the phase. A rejected one leaves the model
|
|
79
|
+
* in `report`, which is what the retry-with-rollback in §6.3 needs: it is
|
|
80
|
+
* re-asked for the same step rather than being let off the hook to act
|
|
81
|
+
* before it has recorded anything.
|
|
82
|
+
*/
|
|
83
|
+
patchApplied(sessionID) {
|
|
84
|
+
this.#phase.set(sessionID, 'act');
|
|
85
|
+
}
|
|
86
|
+
/** Reset a session, on teardown or a fresh run. */
|
|
87
|
+
reset(sessionID) {
|
|
88
|
+
this.#phase.delete(sessionID);
|
|
89
|
+
}
|
|
90
|
+
/** Forget everything. A plugin unload must not leak a phase map. */
|
|
91
|
+
clear() {
|
|
92
|
+
this.#phase.clear();
|
|
93
|
+
}
|
|
94
|
+
/** How many sessions are mid-cycle, for diagnostics and tests. */
|
|
95
|
+
get size() {
|
|
96
|
+
return this.#phase.size;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Whether an action ends the procedure.
|
|
101
|
+
*
|
|
102
|
+
* Shared with the runtime so the two agree on what "finished" means. They
|
|
103
|
+
* answer the same question from the same string, and two copies of that would
|
|
104
|
+
* be two definitions to keep in step.
|
|
105
|
+
*/
|
|
106
|
+
export function isTerminalAction(action) {
|
|
107
|
+
return TERMINAL.has(action.trim().toLowerCase());
|
|
108
|
+
}
|
|
109
|
+
//# sourceMappingURL=step-boundary.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"step-boundary.js","sourceRoot":"","sources":["../src/step-boundary.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAuCG;AAKH,gEAAgE;AAChE,MAAM,QAAQ,GAAG,IAAI,GAAG,CAAC,CAAC,MAAM,EAAE,UAAU,EAAE,WAAW,EAAE,UAAU,EAAE,MAAM,EAAE,KAAK,EAAE,EAAE,CAAC,CAAC,CAAC;AAE3F,MAAM,OAAO,YAAY;IACd,MAAM,GAAG,IAAI,GAAG,EAAiB,CAAC;IAE3C;;;;;OAKG;IACH,MAAM,CAAC,SAAiB;QACtB,OAAO,CAAC,IAAI,CAAC,MAAM,CAAC,GAAG,CAAC,SAAS,CAAC,IAAI,KAAK,CAAC,KAAK,KAAK,CAAC;IACzD,CAAC;IAED;;;;;;;;OAQG;IACH,WAAW,CAAC,SAAiB;QAC3B,IAAI,CAAC,MAAM,CAAC,GAAG,CAAC,SAAS,EAAE,QAAQ,CAAC,CAAC;IACvC,CAAC;IAED;;;;;OAKG;IACH,cAAc,CAAC,SAAiB;QAC9B,OAAO,CAAC,IAAI,CAAC,MAAM,CAAC,GAAG,CAAC,SAAS,CAAC,IAAI,KAAK,CAAC,KAAK,QAAQ,CAAC;IAC5D,CAAC;IAED;;;;;;;OAOG;IACH,YAAY,CAAC,SAAiB;QAC5B,IAAI,CAAC,MAAM,CAAC,GAAG,CAAC,SAAS,EAAE,KAAK,CAAC,CAAC;IACpC,CAAC;IAED,mDAAmD;IACnD,KAAK,CAAC,SAAiB;QACrB,IAAI,CAAC,MAAM,CAAC,MAAM,CAAC,SAAS,CAAC,CAAC;IAChC,CAAC;IAED,oEAAoE;IACpE,KAAK;QACH,IAAI,CAAC,MAAM,CAAC,KAAK,EAAE,CAAC;IACtB,CAAC;IAED,kEAAkE;IAClE,IAAI,IAAI;QACN,OAAO,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC;IAC1B,CAAC;CACF;AAED;;;;;;GAMG;AACH,MAAM,UAAU,gBAAgB,CAAC,MAAc;IAC7C,OAAO,QAAQ,CAAC,GAAG,CAAC,MAAM,CAAC,IAAI,EAAE,CAAC,WAAW,EAAE,CAAC,CAAC;AACnD,CAAC"}
|
package/dist/system-hint.d.ts
CHANGED
|
@@ -34,11 +34,35 @@ export declare const ADVERTISED_TOOLS: readonly ['skillstate_read', 'skillstate_
|
|
|
34
34
|
*
|
|
35
35
|
* Small documents are inlined verbatim (the model sees what is already
|
|
36
36
|
* saved without a tool round-trip). Documents past
|
|
37
|
-
* {@link MAX_INLINE_STATE_CHARS} are summarized as a key list plus a
|
|
38
|
-
*
|
|
39
|
-
*
|
|
37
|
+
* {@link MAX_INLINE_STATE_CHARS} are summarized as a key list plus a count and
|
|
38
|
+
* a pointer to `skillstate_read` — an unbounded state file must not be able to
|
|
39
|
+
* grow the system prompt without limit.
|
|
40
|
+
*
|
|
41
|
+
* The reported count is in the SAME UNIT as the limit, and it is labelled with
|
|
42
|
+
* that unit's name. The first version reported `bytes` next to a limit expressed
|
|
43
|
+
* in characters, which is this project's whole recurring mistake in one line: a
|
|
44
|
+
* state of 4,003 Cyrillic characters is 3,003 over the limit and 7,993 bytes, so
|
|
45
|
+
* the model reading the summary was handed two numbers in two units and a limit
|
|
46
|
+
* in a third. §4.3 is explicit that sizes here are raw string CHARS.
|
|
40
47
|
*/
|
|
41
48
|
export declare function renderStateForHint(state: Record<string, unknown>): string;
|
|
49
|
+
/**
|
|
50
|
+
* How many MODEL REQUESTS may pass with the state untouched before the drift
|
|
51
|
+
* notice fires.
|
|
52
|
+
*
|
|
53
|
+
* **"Requests", not "turns".** Measured on the weakest model in the
|
|
54
|
+
* catalogue: 20 file reads took 6 requests, 40 reads took 12, 70 reads took
|
|
55
|
+
* 27. The model batches tool calls, so a request is worth several file reads
|
|
56
|
+
* and a threshold described in "turns" is off by a factor of three. The
|
|
57
|
+
* number here is the unit the hook can actually count.
|
|
58
|
+
*
|
|
59
|
+
* 12 is chosen from the corpus rather than taste: the average run in the
|
|
60
|
+
* host's own store showed the model ~29,900 prompt tokens per request, so a
|
|
61
|
+
* dozen silent requests is roughly 350k tokens re-sent for a record that
|
|
62
|
+
* never moved. High enough that an agent reading five files in a row is not
|
|
63
|
+
* nagged.
|
|
64
|
+
*/
|
|
65
|
+
export declare const DRIFT_NOTICE_AFTER_TURNS = 12;
|
|
42
66
|
/** Options for {@link buildStateHint}. */
|
|
43
67
|
export interface StateHintOptions {
|
|
44
68
|
/** The state document this session has saved. */
|
|
@@ -50,7 +74,50 @@ export interface StateHintOptions {
|
|
|
50
74
|
* hint names the merge step so the main session can fold its notes back.
|
|
51
75
|
*/
|
|
52
76
|
scope?: string;
|
|
77
|
+
/**
|
|
78
|
+
* The fields a project `skill-spec.json` declares, when one is present.
|
|
79
|
+
*
|
|
80
|
+
* Notes mode does not ENFORCE a schema — §4.1 scopes the schema to a spec P,
|
|
81
|
+
* and notes mode has no P because it never formats an A.4 prompt. What it
|
|
82
|
+
* must not do is stay silent, and it was: a project shipped a schema, a model
|
|
83
|
+
* read it, ignored it, and wrote thirty files under a namespace it invented
|
|
84
|
+
* while the declared fields sat at their defaults. Nothing said so, and the
|
|
85
|
+
* state looked populated to a reader checking the wrong key.
|
|
86
|
+
*
|
|
87
|
+
* So the fields are stated, not enforced. A model that follows them writes
|
|
88
|
+
* the state where the spec says; one that does not has been told where the
|
|
89
|
+
* spec says, which is the difference between an override and an oversight.
|
|
90
|
+
*/
|
|
91
|
+
declaredFields?: readonly string[];
|
|
92
|
+
/**
|
|
93
|
+
* True when the project is INITIALIZED — a state file exists for it.
|
|
94
|
+
*
|
|
95
|
+
* This is the whole difference between an optional side channel and a
|
|
96
|
+
* record, and the two must not be phrased the same way. Telling a model to
|
|
97
|
+
* "skip these tools when the work needs no memory" is correct advice for a
|
|
98
|
+
* scratch project and an invitation to walk away in a real one, where the
|
|
99
|
+
* user initialized skillstate precisely because the work does need it.
|
|
100
|
+
*/
|
|
101
|
+
initialized?: boolean;
|
|
102
|
+
/**
|
|
103
|
+
* Turns taken since the state last changed. Set by the caller, which
|
|
104
|
+
* counts; see {@link DRIFT_NOTICE_AFTER_TURNS}.
|
|
105
|
+
*/
|
|
106
|
+
turnsSinceWrite?: number;
|
|
53
107
|
}
|
|
108
|
+
/**
|
|
109
|
+
* The notice shown once when the state has not moved for a long stretch.
|
|
110
|
+
*
|
|
111
|
+
* This is feedback, not an instruction, and the distinction is the whole
|
|
112
|
+
* design. It states a measured fact — this many turns, no write — and
|
|
113
|
+
* nothing about what the model ought to do. That keeps the v1 failure
|
|
114
|
+
* impossible (nothing here can displace the user's task) while still
|
|
115
|
+
* telling a drifting agent that the user initialized this for a reason.
|
|
116
|
+
*
|
|
117
|
+
* It fires once per silence, not every turn: a notice that repeats forever
|
|
118
|
+
* is wallpaper, and after the second copy nobody reads it.
|
|
119
|
+
*/
|
|
120
|
+
export declare function driftNotice(turnsSinceWrite: number): string;
|
|
54
121
|
/**
|
|
55
122
|
* Build the system-prompt fragment.
|
|
56
123
|
*
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"system-hint.d.ts","sourceRoot":"","sources":["../src/system-hint.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AAEH,mFAAmF;AACnF,eAAO,MAAM,sBAAsB,OAAO,CAAC;AAE3C,yEAAyE;AACzE,eAAO,MAAM,gBAAgB,YAC3B,iBAAiB,EACjB,mBAAmB,EACnB,kBAAkB,CACV,CAAC;AAEX
|
|
1
|
+
{"version":3,"file":"system-hint.d.ts","sourceRoot":"","sources":["../src/system-hint.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AAEH,mFAAmF;AACnF,eAAO,MAAM,sBAAsB,OAAO,CAAC;AAE3C,yEAAyE;AACzE,eAAO,MAAM,gBAAgB,YAC3B,iBAAiB,EACjB,mBAAmB,EACnB,kBAAkB,CACV,CAAC;AAEX;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,MAAM,CAczE;AAED;;;;;;;;;;;;;;;GAeG;AACH,eAAO,MAAM,wBAAwB,KAAK,CAAC;AAE3C,0CAA0C;AAC1C,MAAM,WAAW,gBAAgB;IAC/B,iDAAiD;IACjD,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAC/B,8DAA8D;IAC9D,SAAS,EAAE,MAAM,CAAC;IAClB;;;OAGG;IACH,KAAK,CAAC,EAAE,MAAM,CAAC;IACf;;;;;;;;;;;;;OAaG;IACH,cAAc,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IACnC;;;;;;;;OAQG;IACH,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB;;;OAGG;IACH,eAAe,CAAC,EAAE,MAAM,CAAC;CAC1B;AAmBD;;;;;;;;;;;GAWG;AACH,wBAAgB,WAAW,CAAC,eAAe,EAAE,MAAM,GAAG,MAAM,CAE3D;AAED;;;;;;GAMG;AACH,wBAAgB,cAAc,CAAC,OAAO,EAAE,gBAAgB,GAAG,MAAM,CAkDhE"}
|
package/dist/system-hint.js
CHANGED
|
@@ -38,23 +38,77 @@ export const ADVERTISED_TOOLS = [
|
|
|
38
38
|
*
|
|
39
39
|
* Small documents are inlined verbatim (the model sees what is already
|
|
40
40
|
* saved without a tool round-trip). Documents past
|
|
41
|
-
* {@link MAX_INLINE_STATE_CHARS} are summarized as a key list plus a
|
|
42
|
-
*
|
|
43
|
-
*
|
|
41
|
+
* {@link MAX_INLINE_STATE_CHARS} are summarized as a key list plus a count and
|
|
42
|
+
* a pointer to `skillstate_read` — an unbounded state file must not be able to
|
|
43
|
+
* grow the system prompt without limit.
|
|
44
|
+
*
|
|
45
|
+
* The reported count is in the SAME UNIT as the limit, and it is labelled with
|
|
46
|
+
* that unit's name. The first version reported `bytes` next to a limit expressed
|
|
47
|
+
* in characters, which is this project's whole recurring mistake in one line: a
|
|
48
|
+
* state of 4,003 Cyrillic characters is 3,003 over the limit and 7,993 bytes, so
|
|
49
|
+
* the model reading the summary was handed two numbers in two units and a limit
|
|
50
|
+
* in a third. §4.3 is explicit that sizes here are raw string CHARS.
|
|
44
51
|
*/
|
|
45
52
|
export function renderStateForHint(state) {
|
|
46
53
|
const json = JSON.stringify(state, null, 2);
|
|
47
54
|
if (json.length <= MAX_INLINE_STATE_CHARS)
|
|
48
55
|
return json;
|
|
49
56
|
const keys = Object.keys(state).sort();
|
|
50
|
-
const bytes = Buffer.byteLength(json, 'utf-8');
|
|
51
57
|
return `${JSON.stringify({
|
|
52
58
|
_truncated: true,
|
|
53
|
-
|
|
59
|
+
chars: json.length,
|
|
54
60
|
keys,
|
|
55
61
|
hint: 'Saved state is large — call skillstate_read to load it.',
|
|
56
62
|
}, null, 2)}`;
|
|
57
63
|
}
|
|
64
|
+
/**
|
|
65
|
+
* How many MODEL REQUESTS may pass with the state untouched before the drift
|
|
66
|
+
* notice fires.
|
|
67
|
+
*
|
|
68
|
+
* **"Requests", not "turns".** Measured on the weakest model in the
|
|
69
|
+
* catalogue: 20 file reads took 6 requests, 40 reads took 12, 70 reads took
|
|
70
|
+
* 27. The model batches tool calls, so a request is worth several file reads
|
|
71
|
+
* and a threshold described in "turns" is off by a factor of three. The
|
|
72
|
+
* number here is the unit the hook can actually count.
|
|
73
|
+
*
|
|
74
|
+
* 12 is chosen from the corpus rather than taste: the average run in the
|
|
75
|
+
* host's own store showed the model ~29,900 prompt tokens per request, so a
|
|
76
|
+
* dozen silent requests is roughly 350k tokens re-sent for a record that
|
|
77
|
+
* never moved. High enough that an agent reading five files in a row is not
|
|
78
|
+
* nagged.
|
|
79
|
+
*/
|
|
80
|
+
export const DRIFT_NOTICE_AFTER_TURNS = 12;
|
|
81
|
+
/**
|
|
82
|
+
* The line that says what the state IS, which depends on whether the project
|
|
83
|
+
* was initialized.
|
|
84
|
+
*
|
|
85
|
+
* Neither version is an imperative. Both are statements about the setup, and
|
|
86
|
+
* the difference is that one describes a record the user asked for and the
|
|
87
|
+
* other describes an optional convenience. A model told to skip the tools
|
|
88
|
+
* when it judges them unnecessary will eventually judge a long task
|
|
89
|
+
* unnecessary at exactly the wrong moment; a model told the state is the
|
|
90
|
+
* project's record has a fact to work with.
|
|
91
|
+
*/
|
|
92
|
+
function purposeLine(initialized, statePath) {
|
|
93
|
+
return initialized
|
|
94
|
+
? `This project has an execution state at \`${statePath}\`. It is the project's record: what has been established, decided, and left to do, kept across a context reset or compaction. The conversation is not that record, and it is not kept — the state file is.`
|
|
95
|
+
: `Notes for this project are saved at \`${statePath}\` and survive a context reset or compaction.`;
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* The notice shown once when the state has not moved for a long stretch.
|
|
99
|
+
*
|
|
100
|
+
* This is feedback, not an instruction, and the distinction is the whole
|
|
101
|
+
* design. It states a measured fact — this many turns, no write — and
|
|
102
|
+
* nothing about what the model ought to do. That keeps the v1 failure
|
|
103
|
+
* impossible (nothing here can displace the user's task) while still
|
|
104
|
+
* telling a drifting agent that the user initialized this for a reason.
|
|
105
|
+
*
|
|
106
|
+
* It fires once per silence, not every turn: a notice that repeats forever
|
|
107
|
+
* is wallpaper, and after the second copy nobody reads it.
|
|
108
|
+
*/
|
|
109
|
+
export function driftNotice(turnsSinceWrite) {
|
|
110
|
+
return `Note: this state file has not changed across the last ${turnsSinceWrite} steps of work.`;
|
|
111
|
+
}
|
|
58
112
|
/**
|
|
59
113
|
* Build the system-prompt fragment.
|
|
60
114
|
*
|
|
@@ -74,14 +128,39 @@ export function buildStateHint(options) {
|
|
|
74
128
|
const mergeLine = scope === ''
|
|
75
129
|
? ''
|
|
76
130
|
: '\nThis is a sub-agent session. When you finish, the main session folds your notes back with `skillstate_merge`; write them as if someone else will read them.';
|
|
77
|
-
|
|
131
|
+
const initialized = options.initialized === true;
|
|
132
|
+
const turns = options.turnsSinceWrite ?? 0;
|
|
133
|
+
const declared = options.declaredFields ?? [];
|
|
134
|
+
// Only speak when the state and the spec actually DISAGREE. A line naming
|
|
135
|
+
// the declared fields on every turn of every project would cost prompt budget
|
|
136
|
+
// to say something that is true, and the hint has a standing test that it
|
|
137
|
+
// must stay under a tenth of the conversation it rides along with. The case
|
|
138
|
+
// worth spending characters on is the one measured: a model that wrote
|
|
139
|
+
// everything under a namespace it invented while the declared fields sat at
|
|
140
|
+
// their defaults.
|
|
141
|
+
const undeclared = Object.keys(state).filter((k) => !declared.some((d) => d.startsWith(`${k} `)));
|
|
142
|
+
const schemaLine = declared.length === 0 || undeclared.length === 0
|
|
143
|
+
? ''
|
|
144
|
+
: `\nThis project\'s \`skill-spec.json\` declares the state fields ${declared
|
|
145
|
+
.map((f) => `\`${f}\``)
|
|
146
|
+
.join(', ')}. The state currently holds ${undeclared
|
|
147
|
+
.slice(0, 6)
|
|
148
|
+
.map((k) => `\`${k}\``)
|
|
149
|
+
.join(', ')}, which the spec does not declare. Write declared fields at the TOP LEVEL under their own names; a later reader looking for \`${declared[0].split(' ')[0]}\` will not find it one level down.`;
|
|
150
|
+
const lines = [
|
|
78
151
|
'<skillstate-project-notes>',
|
|
79
|
-
|
|
152
|
+
purposeLine(initialized, statePath),
|
|
80
153
|
'',
|
|
81
154
|
renderStateForHint(state),
|
|
82
|
-
`${toolLine}${mergeLine}`,
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
155
|
+
`${toolLine}${mergeLine}${schemaLine}`,
|
|
156
|
+
initialized
|
|
157
|
+
? 'Record what you establish here as you go — plans made, decisions taken, file paths, values read, and what is left — so it is still here after a reset. The notes are not the task: keep doing what the user asked.'
|
|
158
|
+
: 'Use them only to carry facts across turns — plans already made, decisions already taken, file paths, and what is left to do. The notes are a side channel, not the task: keep doing what the user asked, and skip these tools entirely when the work needs no cross-turn memory.',
|
|
159
|
+
];
|
|
160
|
+
if (initialized && turns >= DRIFT_NOTICE_AFTER_TURNS) {
|
|
161
|
+
lines.push('', driftNotice(turns));
|
|
162
|
+
}
|
|
163
|
+
lines.push('</skillstate-project-notes>');
|
|
164
|
+
return lines.join('\n');
|
|
86
165
|
}
|
|
87
166
|
//# sourceMappingURL=system-hint.js.map
|
package/dist/system-hint.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"system-hint.js","sourceRoot":"","sources":["../src/system-hint.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AAEH,mFAAmF;AACnF,MAAM,CAAC,MAAM,sBAAsB,GAAG,IAAI,CAAC;AAE3C,yEAAyE;AACzE,MAAM,CAAC,MAAM,gBAAgB,GAAG;IAC9B,iBAAiB;IACjB,mBAAmB;IACnB,kBAAkB;CACV,CAAC;AAEX
|
|
1
|
+
{"version":3,"file":"system-hint.js","sourceRoot":"","sources":["../src/system-hint.ts"],"names":[],"mappings":"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"}
|