@skillstate/opencode 2.2.2 → 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 +194 -142
- 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 +58 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +48 -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 +10 -46
- package/dist/opencode-adapter.d.ts.map +1 -1
- package/dist/opencode-adapter.js +1 -64
- 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 +218 -76
- package/dist/plugin.d.ts.map +1 -1
- package/dist/plugin.js +867 -232
- 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/session-registry.d.ts +148 -0
- package/dist/session-registry.d.ts.map +1 -0
- package/dist/session-registry.js +236 -0
- package/dist/session-registry.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/state-store.d.ts +126 -0
- package/dist/state-store.d.ts.map +1 -0
- package/dist/state-store.js +186 -0
- package/dist/state-store.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 +129 -0
- package/dist/system-hint.d.ts.map +1 -0
- package/dist/system-hint.js +166 -0
- package/dist/system-hint.js.map +1 -0
- package/dist/tools.d.ts +148 -0
- package/dist/tools.d.ts.map +1 -0
- package/dist/tools.js +350 -0
- package/dist/tools.js.map +1 -0
- package/package.json +3 -2
- package/dist/plugin-types.d.ts +0 -71
- package/dist/plugin-types.d.ts.map +0 -1
- package/dist/plugin-types.js +0 -8
- package/dist/plugin-types.js.map +0 -1
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Project state persistence for the OpenCode v2 plugin.
|
|
3
|
+
*
|
|
4
|
+
* ONE store, THREE guarantees:
|
|
5
|
+
*
|
|
6
|
+
* 1. **Per-project addressing.** The state file is derived from the plugin
|
|
7
|
+
* instance's location (`ctx.location.project.canonical`), never from
|
|
8
|
+
* `process.cwd()`. The v1 plugin called `process.cwd()` on every hook;
|
|
9
|
+
* in v2 a single OpenCode server serves many projects, so the process
|
|
10
|
+
* cwd is simply the wrong answer. Two checkouts no longer share state.
|
|
11
|
+
*
|
|
12
|
+
* 2. **Per-session isolation.** A root session owns the shared project
|
|
13
|
+
* file; a sub-agent session owns `agents/<scope>/skillstate.json` (see
|
|
14
|
+
* `stateScopeFor`). Parallel sub-agents therefore never last-writer-win
|
|
15
|
+
* over the main session's notes.
|
|
16
|
+
*
|
|
17
|
+
* 3. **Never lose a write.** Every mutation runs inside the core
|
|
18
|
+
* cross-process lock ({@link withStateLock}) and lands via
|
|
19
|
+
* {@link atomicWriteFile} (temp sibling → fsync → rename), so a crash
|
|
20
|
+
* mid-write can never leave a truncated or interleaved state file. Reads
|
|
21
|
+
* never throw: a missing or corrupt file reads as `{}`.
|
|
22
|
+
*
|
|
23
|
+
* The store is deliberately schema-free. It does not validate against a
|
|
24
|
+
* procedural spec: OpenCode sessions are free-form work, and forcing a
|
|
25
|
+
* fixed `goal`/`progress`/`next_steps` shape is what made the v1 prompt
|
|
26
|
+
* rewrite the user's actual task into a state machine.
|
|
27
|
+
*/
|
|
28
|
+
import type { StatePatch } from '@skillstate/core';
|
|
29
|
+
/** A skillstate document: a plain JSON object. */
|
|
30
|
+
export type StateDocument = Record<string, unknown>;
|
|
31
|
+
/** Options for {@link ProjectStateStore}. */
|
|
32
|
+
export interface ProjectStateStoreOptions {
|
|
33
|
+
/**
|
|
34
|
+
* The project directory the state file lives under. Prefer
|
|
35
|
+
* `ctx.location.project.canonical` — the canonical checkout, stable
|
|
36
|
+
* across worktrees.
|
|
37
|
+
*/
|
|
38
|
+
directory: string;
|
|
39
|
+
/** Home directory, for the `$HOME` global bucket. Defaults to `os.homedir()`. */
|
|
40
|
+
home?: string;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* The keys a patch actually changed, split by direction. Mirrors the
|
|
44
|
+
* `state.patch` MCP contract so behaviour is identical across hosts.
|
|
45
|
+
*/
|
|
46
|
+
export interface StateChanges {
|
|
47
|
+
readonly added: readonly string[];
|
|
48
|
+
readonly updated: readonly string[];
|
|
49
|
+
readonly deleted: readonly string[];
|
|
50
|
+
}
|
|
51
|
+
/** Top-level diff between two state documents. Pure. */
|
|
52
|
+
export declare function diffDocuments(before: StateDocument, after: StateDocument): StateChanges;
|
|
53
|
+
/**
|
|
54
|
+
* The state file for one session scope inside one project.
|
|
55
|
+
*
|
|
56
|
+
* `scope === ''` is the shared project file; any other scope is a
|
|
57
|
+
* sub-agent's isolated copy. Delegates to the core resolver (shared with
|
|
58
|
+
* the MCP server, the CLI and the other host adapters) so every host agrees
|
|
59
|
+
* on where a project's state lives.
|
|
60
|
+
*/
|
|
61
|
+
export declare function statePathFor(directory: string, scope: string, home?: string): string;
|
|
62
|
+
/**
|
|
63
|
+
* Reads and merges the per-project state documents for a plugin instance.
|
|
64
|
+
*
|
|
65
|
+
* Stateless between calls apart from the filesystem, so it is safe to keep
|
|
66
|
+
* one instance per plugin and to construct it in tests without any global
|
|
67
|
+
* setup.
|
|
68
|
+
*/
|
|
69
|
+
export declare class ProjectStateStore {
|
|
70
|
+
private readonly directory;
|
|
71
|
+
private readonly home;
|
|
72
|
+
constructor(options: ProjectStateStoreOptions);
|
|
73
|
+
/** The project directory this store addresses. */
|
|
74
|
+
get projectDirectory(): string;
|
|
75
|
+
/** The absolute state file path for a scope (`''` = shared). */
|
|
76
|
+
pathFor(scope: string): string;
|
|
77
|
+
/**
|
|
78
|
+
* Whether a state file already exists for this scope. The plugin uses
|
|
79
|
+
* this to decide whether to inject the system hint at all — an untouched
|
|
80
|
+
* project must behave exactly like vanilla OpenCode.
|
|
81
|
+
*/
|
|
82
|
+
exists(scope: string): boolean;
|
|
83
|
+
/**
|
|
84
|
+
* Read a scope's state. A missing, unreadable or corrupt file reads as
|
|
85
|
+
* `{}` — a best-effort read that never breaks the agent loop.
|
|
86
|
+
*/
|
|
87
|
+
read(scope: string): StateDocument;
|
|
88
|
+
/**
|
|
89
|
+
* Apply a patch to a scope's state and persist it.
|
|
90
|
+
*
|
|
91
|
+
* The read, the merge and the write all happen inside the cross-process
|
|
92
|
+
* lock, so two sub-agents writing disjoint keys both survive — neither
|
|
93
|
+
* can clobber the other's write by reading a stale snapshot.
|
|
94
|
+
*
|
|
95
|
+
* Returns the merged document and the top-level change sets. A `null`
|
|
96
|
+
* value in the patch deletes that key (paper ⊕).
|
|
97
|
+
*/
|
|
98
|
+
patch(scope: string, patch: StatePatch): Promise<{
|
|
99
|
+
state: StateDocument;
|
|
100
|
+
changes: StateChanges;
|
|
101
|
+
}>;
|
|
102
|
+
/**
|
|
103
|
+
* Fold a sub-agent's document into this scope's document.
|
|
104
|
+
*
|
|
105
|
+
* `keep` decides what happens when both sides set the same scalar:
|
|
106
|
+
* `'existing'` (default) keeps the receiving document's value, `'source'`
|
|
107
|
+
* takes the sub-agent's. Keys only present in the sub-agent are always
|
|
108
|
+
* taken, and `null` in either document is a deletion.
|
|
109
|
+
*
|
|
110
|
+
* The source document is left on disk — the merge is not destructive, so
|
|
111
|
+
* a sub-agent's notes remain inspectable after the fold.
|
|
112
|
+
*/
|
|
113
|
+
merge(scope: string, sourceScope: string, keep?: 'existing' | 'source'): Promise<{
|
|
114
|
+
state: StateDocument;
|
|
115
|
+
changes: StateChanges;
|
|
116
|
+
}>;
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* Fold `source` into `target` under a conflict policy. Pure.
|
|
120
|
+
*
|
|
121
|
+
* Nested plain objects recurse. A `null` on either side is a deletion and
|
|
122
|
+
* wins outright — an explicit "remove this" is not a scalar conflict. Every
|
|
123
|
+
* other scalar conflict follows `keep`.
|
|
124
|
+
*/
|
|
125
|
+
export declare function mergeDocuments(target: StateDocument, source: StateDocument, keep: 'existing' | 'source'): StateDocument;
|
|
126
|
+
//# sourceMappingURL=state-store.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"state-store.d.ts","sourceRoot":"","sources":["../src/state-store.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AAaH,OAAO,KAAK,EAAc,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAE/D,kDAAkD;AAClD,MAAM,MAAM,aAAa,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;AAEpD,6CAA6C;AAC7C,MAAM,WAAW,wBAAwB;IACvC;;;;OAIG;IACH,SAAS,EAAE,MAAM,CAAC;IAClB,iFAAiF;IACjF,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAED;;;GAGG;AACH,MAAM,WAAW,YAAY;IAC3B,QAAQ,CAAC,KAAK,EAAE,SAAS,MAAM,EAAE,CAAC;IAClC,QAAQ,CAAC,OAAO,EAAE,SAAS,MAAM,EAAE,CAAC;IACpC,QAAQ,CAAC,OAAO,EAAE,SAAS,MAAM,EAAE,CAAC;CACrC;AAED,wDAAwD;AACxD,wBAAgB,aAAa,CAC3B,MAAM,EAAE,aAAa,EACrB,KAAK,EAAE,aAAa,GACnB,YAAY,CAed;AAED;;;;;;;GAOG;AACH,wBAAgB,YAAY,CAC1B,SAAS,EAAE,MAAM,EACjB,KAAK,EAAE,MAAM,EACb,IAAI,GAAE,MAAqB,GAC1B,MAAM,CAER;AAED;;;;;;GAMG;AACH,qBAAa,iBAAiB;IAC5B,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAS;IACnC,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAS;IAE9B,YAAY,OAAO,EAAE,wBAAwB,EAG5C;IAED,kDAAkD;IAClD,IAAI,gBAAgB,IAAI,MAAM,CAE7B;IAED,gEAAgE;IAChE,OAAO,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAE7B;IAED;;;;OAIG;IACH,MAAM,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAM7B;IAED;;;OAGG;IACH,IAAI,CAAC,KAAK,EAAE,MAAM,GAAG,aAAa,CAQjC;IAED;;;;;;;;;OASG;IACG,KAAK,CACT,KAAK,EAAE,MAAM,EACb,KAAK,EAAE,UAAU,GAChB,OAAO,CAAC;QAAE,KAAK,EAAE,aAAa,CAAC;QAAC,OAAO,EAAE,YAAY,CAAA;KAAE,CAAC,CAY1D;IAED;;;;;;;;;;OAUG;IACG,KAAK,CACT,KAAK,EAAE,MAAM,EACb,WAAW,EAAE,MAAM,EACnB,IAAI,GAAE,UAAU,GAAG,QAAqB,GACvC,OAAO,CAAC;QAAE,KAAK,EAAE,aAAa,CAAC;QAAC,OAAO,EAAE,YAAY,CAAA;KAAE,CAAC,CAoB1D;CACF;AAED;;;;;;GAMG;AACH,wBAAgB,cAAc,CAC5B,MAAM,EAAE,aAAa,EACrB,MAAM,EAAE,aAAa,EACrB,IAAI,EAAE,UAAU,GAAG,QAAQ,GAC1B,aAAa,CAsBf"}
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Project state persistence for the OpenCode v2 plugin.
|
|
3
|
+
*
|
|
4
|
+
* ONE store, THREE guarantees:
|
|
5
|
+
*
|
|
6
|
+
* 1. **Per-project addressing.** The state file is derived from the plugin
|
|
7
|
+
* instance's location (`ctx.location.project.canonical`), never from
|
|
8
|
+
* `process.cwd()`. The v1 plugin called `process.cwd()` on every hook;
|
|
9
|
+
* in v2 a single OpenCode server serves many projects, so the process
|
|
10
|
+
* cwd is simply the wrong answer. Two checkouts no longer share state.
|
|
11
|
+
*
|
|
12
|
+
* 2. **Per-session isolation.** A root session owns the shared project
|
|
13
|
+
* file; a sub-agent session owns `agents/<scope>/skillstate.json` (see
|
|
14
|
+
* `stateScopeFor`). Parallel sub-agents therefore never last-writer-win
|
|
15
|
+
* over the main session's notes.
|
|
16
|
+
*
|
|
17
|
+
* 3. **Never lose a write.** Every mutation runs inside the core
|
|
18
|
+
* cross-process lock ({@link withStateLock}) and lands via
|
|
19
|
+
* {@link atomicWriteFile} (temp sibling → fsync → rename), so a crash
|
|
20
|
+
* mid-write can never leave a truncated or interleaved state file. Reads
|
|
21
|
+
* never throw: a missing or corrupt file reads as `{}`.
|
|
22
|
+
*
|
|
23
|
+
* The store is deliberately schema-free. It does not validate against a
|
|
24
|
+
* procedural spec: OpenCode sessions are free-form work, and forcing a
|
|
25
|
+
* fixed `goal`/`progress`/`next_steps` shape is what made the v1 prompt
|
|
26
|
+
* rewrite the user's actual task into a state machine.
|
|
27
|
+
*/
|
|
28
|
+
import * as fs from 'node:fs';
|
|
29
|
+
import * as os from 'node:os';
|
|
30
|
+
import * as path from 'node:path';
|
|
31
|
+
import { atomicWriteFile, isPlainObject, mergePatch, migrate, resolveHostStateForCwd, withStateLock, } from '@skillstate/core';
|
|
32
|
+
/** Top-level diff between two state documents. Pure. */
|
|
33
|
+
export function diffDocuments(before, after) {
|
|
34
|
+
const added = [];
|
|
35
|
+
const updated = [];
|
|
36
|
+
const deleted = [];
|
|
37
|
+
for (const key of Object.keys(after)) {
|
|
38
|
+
if (!(key in before)) {
|
|
39
|
+
added.push(key);
|
|
40
|
+
}
|
|
41
|
+
else if (JSON.stringify(before[key]) !== JSON.stringify(after[key])) {
|
|
42
|
+
updated.push(key);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
for (const key of Object.keys(before)) {
|
|
46
|
+
if (!(key in after))
|
|
47
|
+
deleted.push(key);
|
|
48
|
+
}
|
|
49
|
+
return { added, updated, deleted };
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* The state file for one session scope inside one project.
|
|
53
|
+
*
|
|
54
|
+
* `scope === ''` is the shared project file; any other scope is a
|
|
55
|
+
* sub-agent's isolated copy. Delegates to the core resolver (shared with
|
|
56
|
+
* the MCP server, the CLI and the other host adapters) so every host agrees
|
|
57
|
+
* on where a project's state lives.
|
|
58
|
+
*/
|
|
59
|
+
export function statePathFor(directory, scope, home = os.homedir()) {
|
|
60
|
+
return resolveHostStateForCwd(directory, home, scope);
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Reads and merges the per-project state documents for a plugin instance.
|
|
64
|
+
*
|
|
65
|
+
* Stateless between calls apart from the filesystem, so it is safe to keep
|
|
66
|
+
* one instance per plugin and to construct it in tests without any global
|
|
67
|
+
* setup.
|
|
68
|
+
*/
|
|
69
|
+
export class ProjectStateStore {
|
|
70
|
+
directory;
|
|
71
|
+
home;
|
|
72
|
+
constructor(options) {
|
|
73
|
+
this.directory = path.resolve(options.directory);
|
|
74
|
+
this.home = options.home ?? os.homedir();
|
|
75
|
+
}
|
|
76
|
+
/** The project directory this store addresses. */
|
|
77
|
+
get projectDirectory() {
|
|
78
|
+
return this.directory;
|
|
79
|
+
}
|
|
80
|
+
/** The absolute state file path for a scope (`''` = shared). */
|
|
81
|
+
pathFor(scope) {
|
|
82
|
+
return statePathFor(this.directory, scope, this.home);
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Whether a state file already exists for this scope. The plugin uses
|
|
86
|
+
* this to decide whether to inject the system hint at all — an untouched
|
|
87
|
+
* project must behave exactly like vanilla OpenCode.
|
|
88
|
+
*/
|
|
89
|
+
exists(scope) {
|
|
90
|
+
try {
|
|
91
|
+
return fs.statSync(this.pathFor(scope)).isFile();
|
|
92
|
+
}
|
|
93
|
+
catch {
|
|
94
|
+
return false;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Read a scope's state. A missing, unreadable or corrupt file reads as
|
|
99
|
+
* `{}` — a best-effort read that never breaks the agent loop.
|
|
100
|
+
*/
|
|
101
|
+
read(scope) {
|
|
102
|
+
const filePath = this.pathFor(scope);
|
|
103
|
+
try {
|
|
104
|
+
const raw = fs.readFileSync(filePath, 'utf-8');
|
|
105
|
+
return migrate(JSON.parse(raw)).state;
|
|
106
|
+
}
|
|
107
|
+
catch {
|
|
108
|
+
return {};
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* Apply a patch to a scope's state and persist it.
|
|
113
|
+
*
|
|
114
|
+
* The read, the merge and the write all happen inside the cross-process
|
|
115
|
+
* lock, so two sub-agents writing disjoint keys both survive — neither
|
|
116
|
+
* can clobber the other's write by reading a stale snapshot.
|
|
117
|
+
*
|
|
118
|
+
* Returns the merged document and the top-level change sets. A `null`
|
|
119
|
+
* value in the patch deletes that key (paper ⊕).
|
|
120
|
+
*/
|
|
121
|
+
async patch(scope, patch) {
|
|
122
|
+
const filePath = this.pathFor(scope);
|
|
123
|
+
const result = await withStateLock(filePath, async () => {
|
|
124
|
+
const before = this.read(scope);
|
|
125
|
+
const after = mergePatch(before, patch);
|
|
126
|
+
await atomicWriteFile(filePath, `${JSON.stringify({ version: 1, state: after }, null, 2)}\n`);
|
|
127
|
+
return { state: after, changes: diffDocuments(before, after) };
|
|
128
|
+
});
|
|
129
|
+
return result;
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* Fold a sub-agent's document into this scope's document.
|
|
133
|
+
*
|
|
134
|
+
* `keep` decides what happens when both sides set the same scalar:
|
|
135
|
+
* `'existing'` (default) keeps the receiving document's value, `'source'`
|
|
136
|
+
* takes the sub-agent's. Keys only present in the sub-agent are always
|
|
137
|
+
* taken, and `null` in either document is a deletion.
|
|
138
|
+
*
|
|
139
|
+
* The source document is left on disk — the merge is not destructive, so
|
|
140
|
+
* a sub-agent's notes remain inspectable after the fold.
|
|
141
|
+
*/
|
|
142
|
+
async merge(scope, sourceScope, keep = 'existing') {
|
|
143
|
+
if (sourceScope === scope) {
|
|
144
|
+
throw new Error('skillstate_merge: a sub-agent cannot merge its own state');
|
|
145
|
+
}
|
|
146
|
+
const source = this.read(sourceScope);
|
|
147
|
+
if (Object.keys(source).length === 0) {
|
|
148
|
+
throw new Error(`skillstate_merge: no saved state for sub-agent "${sourceScope}" — nothing to merge`);
|
|
149
|
+
}
|
|
150
|
+
const filePath = this.pathFor(scope);
|
|
151
|
+
return withStateLock(filePath, async () => {
|
|
152
|
+
const before = this.read(scope);
|
|
153
|
+
const after = mergeDocuments(before, source, keep);
|
|
154
|
+
await atomicWriteFile(filePath, `${JSON.stringify({ version: 1, state: after }, null, 2)}\n`);
|
|
155
|
+
return { state: after, changes: diffDocuments(before, after) };
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
/**
|
|
160
|
+
* Fold `source` into `target` under a conflict policy. Pure.
|
|
161
|
+
*
|
|
162
|
+
* Nested plain objects recurse. A `null` on either side is a deletion and
|
|
163
|
+
* wins outright — an explicit "remove this" is not a scalar conflict. Every
|
|
164
|
+
* other scalar conflict follows `keep`.
|
|
165
|
+
*/
|
|
166
|
+
export function mergeDocuments(target, source, keep) {
|
|
167
|
+
const result = { ...target };
|
|
168
|
+
for (const [key, value] of Object.entries(source)) {
|
|
169
|
+
if (value === null) {
|
|
170
|
+
delete result[key];
|
|
171
|
+
continue;
|
|
172
|
+
}
|
|
173
|
+
if (!(key in result)) {
|
|
174
|
+
result[key] = value;
|
|
175
|
+
continue;
|
|
176
|
+
}
|
|
177
|
+
if (isPlainObject(value) && isPlainObject(result[key])) {
|
|
178
|
+
result[key] = mergeDocuments(result[key], value, keep);
|
|
179
|
+
continue;
|
|
180
|
+
}
|
|
181
|
+
if (keep === 'source')
|
|
182
|
+
result[key] = value;
|
|
183
|
+
}
|
|
184
|
+
return result;
|
|
185
|
+
}
|
|
186
|
+
//# sourceMappingURL=state-store.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"state-store.js","sourceRoot":"","sources":["../src/state-store.ts"],"names":[],"mappings":"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"}
|
|
@@ -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"}
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The system-prompt fragment the plugin contributes to each model request.
|
|
3
|
+
*
|
|
4
|
+
* ── Why this module is so small and so careful ───────────────────────────
|
|
5
|
+
*
|
|
6
|
+
* The v1 integration rewrote the conversation on every turn: it truncated
|
|
7
|
+
* history to the last three messages and appended a synthetic `role: "user"`
|
|
8
|
+
* message whose body was the raw state JSON. The reported symptom was that
|
|
9
|
+
* the agent stopped doing what it was asked and started emitting state
|
|
10
|
+
* JSON instead. The cause was structural, not a model quirk:
|
|
11
|
+
*
|
|
12
|
+
* - The last `role: "user"` message is what the model treats as the current
|
|
13
|
+
* instruction, so the state blob displaced the user's actual request.
|
|
14
|
+
* - Truncating history deleted the task statement, the tool results, and
|
|
15
|
+
* the errors the agent had just been given — it could not reason about
|
|
16
|
+
* work it could no longer see.
|
|
17
|
+
*
|
|
18
|
+
* The fix is a rule, enforced by `tests/opencode/context-integrity.test.ts`:
|
|
19
|
+
* **this plugin never mutates `event.messages`.** It contributes one
|
|
20
|
+
* additive, bounded fragment to `event.system`, states what the tools are
|
|
21
|
+
* for, and explicitly leaves the task alone.
|
|
22
|
+
*
|
|
23
|
+
* The wording below is a product requirement, not prose taste. It must
|
|
24
|
+
* describe the tools without instructing the model to change how it works.
|
|
25
|
+
* Imperative framing ("you must", "always", "respond with a JSON block")
|
|
26
|
+
* is what turns a persistence aid into a prompt override.
|
|
27
|
+
*/
|
|
28
|
+
/** Largest state JSON rendered into the hint before falling back to a key list. */
|
|
29
|
+
export declare const MAX_INLINE_STATE_CHARS = 4000;
|
|
30
|
+
/** The tool names the hint advertises — kept in sync with `tools.ts`. */
|
|
31
|
+
export declare const ADVERTISED_TOOLS: readonly ['skillstate_read', 'skillstate_update', 'skillstate_merge'];
|
|
32
|
+
/**
|
|
33
|
+
* Render a state document for inclusion in the system prompt.
|
|
34
|
+
*
|
|
35
|
+
* Small documents are inlined verbatim (the model sees what is already
|
|
36
|
+
* saved without a tool round-trip). Documents past
|
|
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.
|
|
47
|
+
*/
|
|
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;
|
|
66
|
+
/** Options for {@link buildStateHint}. */
|
|
67
|
+
export interface StateHintOptions {
|
|
68
|
+
/** The state document this session has saved. */
|
|
69
|
+
state: Record<string, unknown>;
|
|
70
|
+
/** Project-relative state path, for the model's reference. */
|
|
71
|
+
statePath: string;
|
|
72
|
+
/**
|
|
73
|
+
* This session's scope, or `''` when it is a root session. A sub-agent
|
|
74
|
+
* hint names the merge step so the main session can fold its notes back.
|
|
75
|
+
*/
|
|
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;
|
|
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;
|
|
121
|
+
/**
|
|
122
|
+
* Build the system-prompt fragment.
|
|
123
|
+
*
|
|
124
|
+
* Returns `''` for an empty document so an untouched project contributes
|
|
125
|
+
* nothing at all — the plugin is inert until the agent actually saves
|
|
126
|
+
* something, and an inert plugin is indistinguishable from no plugin.
|
|
127
|
+
*/
|
|
128
|
+
export declare function buildStateHint(options: StateHintOptions): string;
|
|
129
|
+
//# sourceMappingURL=system-hint.d.ts.map
|
|
@@ -0,0 +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;;;;;;;;;;;;;;;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"}
|