@skillstate/opencode 2.0.3 → 2.0.4
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 +17 -5
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/opencode-adapter.d.ts +28 -17
- package/dist/opencode-adapter.d.ts.map +1 -1
- package/dist/opencode-adapter.js +105 -67
- package/dist/opencode-adapter.js.map +1 -1
- package/dist/plugin-types.d.ts +64 -0
- package/dist/plugin-types.d.ts.map +1 -0
- package/dist/plugin-types.js +8 -0
- package/dist/plugin-types.js.map +1 -0
- package/dist/plugin.d.ts +38 -0
- package/dist/plugin.d.ts.map +1 -0
- package/dist/plugin.js +179 -0
- package/dist/plugin.js.map +1 -0
- package/package.json +5 -1
package/README.md
CHANGED
|
@@ -121,16 +121,24 @@ synthetic `{ info, parts }` message.
|
|
|
121
121
|
|
|
122
122
|
## API / Exports
|
|
123
123
|
|
|
124
|
-
Root path `@skillstate/opencode` exports
|
|
124
|
+
Root path `@skillstate/opencode` exports the adapter and the static plugin:
|
|
125
125
|
|
|
126
126
|
- `new OpenCodeAdapter()` — implements `PlatformAdapter` (`name = 'opencode'`).
|
|
127
127
|
- `generateSkillMd(spec, statePath?): string` — a `SKILL.md` body with
|
|
128
128
|
frontmatter and a state-based process description.
|
|
129
|
-
- `generatePluginCode(statePath, options?): string` —
|
|
130
|
-
(`
|
|
129
|
+
- `generatePluginCode(statePath, options?): string` — a thin plugin loader by
|
|
130
|
+
default (`import { createSkillStatePlugin } from '@skillstate/opencode'` with
|
|
131
|
+
the state path baked in; `options.maxHistoryMessages`, default 3).
|
|
132
|
+
`options.standalone: true` inlines the full self-contained plugin — escape
|
|
133
|
+
hatch for environments without npm resolution of this package. Hooks:
|
|
131
134
|
`experimental.chat.messages.transform` (real history trimming),
|
|
132
135
|
`experimental.session.compacting` (inject state into compaction context),
|
|
133
136
|
`tool.execute.after` (persist `state_patch` to disk).
|
|
137
|
+
- `createSkillStatePlugin({ statePath, maxHistoryMessages? })` — the static
|
|
138
|
+
plugin factory (single source of truth for the hook logic); re-exported from
|
|
139
|
+
`@skillstate/opencode/plugin` as well.
|
|
140
|
+
- `readSkillState` / `saveSkillState` / `mergePatch` / `extractPatch` — the
|
|
141
|
+
plugin's state helpers, shared by the static plugin.
|
|
134
142
|
- `savePluginCode(target, statePath, options?): Promise<string>` — writes the
|
|
135
143
|
plugin atomically and returns the destination.
|
|
136
144
|
- `injectState(state, spec): string` / `formatPrompt(state, observation, spec): string`.
|
|
@@ -145,8 +153,12 @@ Both `generatePluginCode`/`savePluginCode` accept a raw path (legacy) or a
|
|
|
145
153
|
`experimental.chat.messages.transform`, so the plugin drops old messages
|
|
146
154
|
instead of just hiding them — only the last N non-system messages plus an
|
|
147
155
|
injected state message reach the LLM.
|
|
148
|
-
- The generated plugin is a
|
|
149
|
-
|
|
156
|
+
- The default generated plugin is a **thin loader**: it imports
|
|
157
|
+
`createSkillStatePlugin` from `@skillstate/opencode` (one source of truth)
|
|
158
|
+
and bakes in the state path. The `{ standalone: true }` template is a
|
|
159
|
+
self-contained ESM/TS escape hatch for environments without npm resolution;
|
|
160
|
+
hook logic lives only in `src/plugin.ts` — regenerate rather than editing
|
|
161
|
+
generated files.
|
|
150
162
|
- Depends on [`@skillstate/core`](../core) for `PromptTransformer`,
|
|
151
163
|
`atomicWriteFile`, and `resolveStatePath`.
|
|
152
164
|
|
package/dist/index.d.ts
CHANGED
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,cAAc,uBAAuB,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,cAAc,uBAAuB,CAAC;AACtC,cAAc,aAAa,CAAC"}
|
package/dist/index.js
CHANGED
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,oDAAoD;AACpD,cAAc,uBAAuB,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,oDAAoD;AACpD,cAAc,uBAAuB,CAAC;AACtC,cAAc,aAAa,CAAC"}
|
|
@@ -14,6 +14,16 @@
|
|
|
14
14
|
*/
|
|
15
15
|
import type { SkillState, StatePatch, ProceduralSpec, Observation, PlatformAdapter } from '@skillstate/core';
|
|
16
16
|
import type { StatePathRef } from '@skillstate/core';
|
|
17
|
+
/** Options for {@link OpenCodeAdapter.generatePluginCode}. */
|
|
18
|
+
export interface GeneratePluginOptions {
|
|
19
|
+
/** Non-system messages kept by the plugin (default 3). */
|
|
20
|
+
maxHistoryMessages?: number;
|
|
21
|
+
/**
|
|
22
|
+
* Inline the full plugin instead of the thin loader — escape hatch for
|
|
23
|
+
* environments without npm resolution of `@skillstate/opencode`.
|
|
24
|
+
*/
|
|
25
|
+
standalone?: boolean;
|
|
26
|
+
}
|
|
17
27
|
/**
|
|
18
28
|
* OpenCode platform adapter (@non-paper; see module doc).
|
|
19
29
|
*/
|
|
@@ -51,27 +61,30 @@ export declare class OpenCodeAdapter implements PlatformAdapter {
|
|
|
51
61
|
*/
|
|
52
62
|
generateSkillMd(spec: ProceduralSpec, statePath?: string): string;
|
|
53
63
|
/**
|
|
54
|
-
* Generate an OpenCode plugin
|
|
64
|
+
* Generate an OpenCode plugin file.
|
|
65
|
+
*
|
|
66
|
+
* Default (thin) mode: a loader that imports the STATIC plugin
|
|
67
|
+
* (`createSkillStatePlugin` from `@skillstate/opencode`) — the single
|
|
68
|
+
* source of truth for hook logic. `statePath`/`maxHistoryMessages` are
|
|
69
|
+
* baked into the loader call.
|
|
70
|
+
*
|
|
71
|
+
* `{ standalone: true }` inlines the full plugin (self-contained template,
|
|
72
|
+
* escape hatch for environments without npm resolution of
|
|
73
|
+
* `@skillstate/opencode`).
|
|
55
74
|
*
|
|
56
|
-
* Hooks:
|
|
57
|
-
*
|
|
58
|
-
*
|
|
59
|
-
*
|
|
60
|
-
*
|
|
61
|
-
* context so the summary preserves it.
|
|
62
|
-
* - `tool.execute.after`: persists state updates from LLM responses.
|
|
75
|
+
* Hooks (see `src/plugin.ts`): `experimental.chat.messages.transform`
|
|
76
|
+
* (real O(1) history trimming, `{ info, parts }` entries, in-place
|
|
77
|
+
* mutation), `experimental.session.compacting` (state into compaction
|
|
78
|
+
* context), `tool.execute.after` (persist `state_patch` from
|
|
79
|
+
* `output.output`).
|
|
63
80
|
*/
|
|
64
|
-
generatePluginCode(statePath: string, options?:
|
|
65
|
-
maxHistoryMessages?: number;
|
|
66
|
-
}): string;
|
|
81
|
+
generatePluginCode(statePath: string, options?: GeneratePluginOptions): string;
|
|
67
82
|
/**
|
|
68
83
|
* @non-paper additive overload: accept a `{ root, name }` ref confined
|
|
69
84
|
* via `resolveStatePath` — `..` escapes throw instead of embedding an
|
|
70
85
|
* unsafe path into the generated plugin.
|
|
71
86
|
*/
|
|
72
|
-
generatePluginCode(stateRef: StatePathRef, options?:
|
|
73
|
-
maxHistoryMessages?: number;
|
|
74
|
-
}): string;
|
|
87
|
+
generatePluginCode(stateRef: StatePathRef, options?: GeneratePluginOptions): string;
|
|
75
88
|
/**
|
|
76
89
|
* @non-paper additive helper: generate the plugin and persist it via
|
|
77
90
|
* `atomicWriteFile` (tmp + fsync + rename). Both the destination and the
|
|
@@ -79,9 +92,7 @@ export declare class OpenCodeAdapter implements PlatformAdapter {
|
|
|
79
92
|
* `{ root, name }` refs confined by `resolveStatePath`. Returns the
|
|
80
93
|
* absolute destination path.
|
|
81
94
|
*/
|
|
82
|
-
savePluginCode(target: string | StatePathRef, statePath: string | StatePathRef, options?:
|
|
83
|
-
maxHistoryMessages?: number;
|
|
84
|
-
}): Promise<string>;
|
|
95
|
+
savePluginCode(target: string | StatePathRef, statePath: string | StatePathRef, options?: GeneratePluginOptions): Promise<string>;
|
|
85
96
|
/**
|
|
86
97
|
* Describe the schema fields for inclusion in prompts.
|
|
87
98
|
*/
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"opencode-adapter.d.ts","sourceRoot":"","sources":["../src/opencode-adapter.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AACH,OAAO,KAAK,EACV,UAAU,EACV,UAAU,EACV,cAAc,EACd,WAAW,EACX,eAAe,EAChB,MAAM,kBAAkB,CAAC;AAM1B,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,kBAAkB,CAAC;AAErD;;GAEG;AACH,qBAAa,eAAgB,YAAW,eAAe;IACrD,QAAQ,CAAC,IAAI,cAAc;IAE3B,OAAO,CAAC,WAAW,CAAmD;IAEtE;;;;OAIG;IACH,WAAW,CAAC,KAAK,EAAE,UAAU,EAAE,IAAI,EAAE,cAAc,GAAG,MAAM,CAsB3D;IAED;;OAEG;IACH,YAAY,CAAC,QAAQ,EAAE,MAAM,GAAG,UAAU,GAAG,IAAI,CAEhD;IAED;;OAEG;IACH,aAAa,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAE7C;IAED;;;;;OAKG;IACH,YAAY,CACV,KAAK,EAAE,UAAU,EACjB,WAAW,EAAE,WAAW,EACxB,IAAI,EAAE,cAAc,GACnB,MAAM,CAER;IAED;;;;;;;OAOG;IACH,eAAe,CAAC,IAAI,EAAE,cAAc,EAAE,SAAS,CAAC,EAAE,MAAM,GAAG,MAAM,CA0ChE;IAED
|
|
1
|
+
{"version":3,"file":"opencode-adapter.d.ts","sourceRoot":"","sources":["../src/opencode-adapter.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AACH,OAAO,KAAK,EACV,UAAU,EACV,UAAU,EACV,cAAc,EACd,WAAW,EACX,eAAe,EAChB,MAAM,kBAAkB,CAAC;AAM1B,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,kBAAkB,CAAC;AAErD,8DAA8D;AAC9D,MAAM,WAAW,qBAAqB;IACpC,0DAA0D;IAC1D,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B;;;OAGG;IACH,UAAU,CAAC,EAAE,OAAO,CAAC;CACtB;AAED;;GAEG;AACH,qBAAa,eAAgB,YAAW,eAAe;IACrD,QAAQ,CAAC,IAAI,cAAc;IAE3B,OAAO,CAAC,WAAW,CAAmD;IAEtE;;;;OAIG;IACH,WAAW,CAAC,KAAK,EAAE,UAAU,EAAE,IAAI,EAAE,cAAc,GAAG,MAAM,CAsB3D;IAED;;OAEG;IACH,YAAY,CAAC,QAAQ,EAAE,MAAM,GAAG,UAAU,GAAG,IAAI,CAEhD;IAED;;OAEG;IACH,aAAa,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAE7C;IAED;;;;;OAKG;IACH,YAAY,CACV,KAAK,EAAE,UAAU,EACjB,WAAW,EAAE,WAAW,EACxB,IAAI,EAAE,cAAc,GACnB,MAAM,CAER;IAED;;;;;;;OAOG;IACH,eAAe,CAAC,IAAI,EAAE,cAAc,EAAE,SAAS,CAAC,EAAE,MAAM,GAAG,MAAM,CA0ChE;IAED;;;;;;;;;;;;;;;;;OAiBG;IACH,kBAAkB,CAAC,SAAS,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,qBAAqB,GAAG,MAAM,CAAC;IAC/E;;;;OAIG;IACH,kBAAkB,CAAC,QAAQ,EAAE,YAAY,EAAE,OAAO,CAAC,EAAE,qBAAqB,GAAG,MAAM,CAAC;IA4BpF;;;;;;OAMG;IACG,cAAc,CAClB,MAAM,EAAE,MAAM,GAAG,YAAY,EAC7B,SAAS,EAAE,MAAM,GAAG,YAAY,EAChC,OAAO,CAAC,EAAE,qBAAqB,GAC9B,OAAO,CAAC,MAAM,CAAC,CAYjB;IAMD;;OAEG;IACH,OAAO,CAAC,cAAc;CASvB"}
|
package/dist/opencode-adapter.js
CHANGED
|
@@ -108,9 +108,63 @@ plugin; only the current state and the latest observation matter.
|
|
|
108
108
|
const statePath = typeof statePathOrRef === 'string'
|
|
109
109
|
? statePathOrRef
|
|
110
110
|
: resolveStatePath(statePathOrRef.root, statePathOrRef.name);
|
|
111
|
-
const sp = JSON.stringify(statePath);
|
|
112
111
|
const maxHistory = options?.maxHistoryMessages ?? 3;
|
|
113
|
-
|
|
112
|
+
if (options?.standalone) {
|
|
113
|
+
return standalonePluginCode(statePath, maxHistory);
|
|
114
|
+
}
|
|
115
|
+
const sp = JSON.stringify(statePath);
|
|
116
|
+
return `// OpenCode plugin (thin loader) generated by skillstate.
|
|
117
|
+
// Hook logic lives in the static plugin — the single source of truth —
|
|
118
|
+
// shipped in the @skillstate/opencode package.
|
|
119
|
+
import { createSkillStatePlugin } from '@skillstate/opencode';
|
|
120
|
+
|
|
121
|
+
export default createSkillStatePlugin({
|
|
122
|
+
statePath: ${sp},
|
|
123
|
+
maxHistoryMessages: ${maxHistory},
|
|
124
|
+
});
|
|
125
|
+
`;
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* @non-paper additive helper: generate the plugin and persist it via
|
|
129
|
+
* `atomicWriteFile` (tmp + fsync + rename). Both the destination and the
|
|
130
|
+
* embedded state path accept raw strings (legacy behavior) or
|
|
131
|
+
* `{ root, name }` refs confined by `resolveStatePath`. Returns the
|
|
132
|
+
* absolute destination path.
|
|
133
|
+
*/
|
|
134
|
+
async savePluginCode(target, statePath, options) {
|
|
135
|
+
const dest = typeof target === 'string'
|
|
136
|
+
? target
|
|
137
|
+
: resolveStatePath(target.root, target.name);
|
|
138
|
+
const resolvedState = typeof statePath === 'string'
|
|
139
|
+
? statePath
|
|
140
|
+
: resolveStatePath(statePath.root, statePath.name);
|
|
141
|
+
const plugin = this.generatePluginCode(resolvedState, options);
|
|
142
|
+
await atomicWriteFile(dest, plugin);
|
|
143
|
+
return dest;
|
|
144
|
+
}
|
|
145
|
+
/* ------------------------------------------------------------------ */
|
|
146
|
+
/* Internal helpers */
|
|
147
|
+
/* ------------------------------------------------------------------ */
|
|
148
|
+
/**
|
|
149
|
+
* Describe the schema fields for inclusion in prompts.
|
|
150
|
+
*/
|
|
151
|
+
describeSchema(schema) {
|
|
152
|
+
const fields = Object.entries(schema)
|
|
153
|
+
.map(([name, field]) => `- ${name} (${field.type}): ${field.description ?? 'no description'}`)
|
|
154
|
+
.join('\n');
|
|
155
|
+
return `## Schema\n${fields}`;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
/**
|
|
159
|
+
* Standalone (escape-hatch) plugin template — a self-contained inline copy of
|
|
160
|
+
* `src/plugin.ts` for environments without npm resolution of
|
|
161
|
+
* `@skillstate/opencode`. Escape hatch only: `src/plugin.ts` remains the
|
|
162
|
+
* single source of truth; regenerate rather than editing generated files.
|
|
163
|
+
*/
|
|
164
|
+
function standalonePluginCode(statePath, maxHistory) {
|
|
165
|
+
const sp = JSON.stringify(statePath);
|
|
166
|
+
return `// OpenCode plugin (standalone escape hatch) generated by skillstate.
|
|
167
|
+
// Self-contained copy of createSkillStatePlugin from @skillstate/opencode.
|
|
114
168
|
// Real O(1) prompt economy via experimental.chat.messages.transform:
|
|
115
169
|
// trims history to last ${maxHistory} non-system messages + injected state.
|
|
116
170
|
|
|
@@ -139,20 +193,20 @@ function saveSkillState(state: Record<string, unknown>): void {
|
|
|
139
193
|
}
|
|
140
194
|
}
|
|
141
195
|
|
|
196
|
+
function isPlainObject(value: unknown): value is Record<string, unknown> {
|
|
197
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
198
|
+
}
|
|
199
|
+
|
|
142
200
|
function mergePatch(base: Record<string, unknown>, patch: Record<string, unknown>): Record<string, unknown> {
|
|
143
201
|
const result = { ...base };
|
|
144
202
|
for (const key of Object.keys(patch)) {
|
|
145
|
-
|
|
203
|
+
const value = patch[key];
|
|
204
|
+
if (value === null) {
|
|
146
205
|
delete result[key];
|
|
147
|
-
} else if (
|
|
148
|
-
|
|
149
|
-
!Array.isArray(patch[key]) &&
|
|
150
|
-
typeof result[key] === "object" && result[key] !== null &&
|
|
151
|
-
!Array.isArray(result[key])
|
|
152
|
-
) {
|
|
153
|
-
result[key] = mergePatch(result[key] as Record<string, unknown>, patch[key] as Record<string, unknown>);
|
|
206
|
+
} else if (isPlainObject(value) && isPlainObject(result[key])) {
|
|
207
|
+
result[key] = mergePatch(result[key] as Record<string, unknown>, value);
|
|
154
208
|
} else {
|
|
155
|
-
result[key] =
|
|
209
|
+
result[key] = value;
|
|
156
210
|
}
|
|
157
211
|
}
|
|
158
212
|
return result;
|
|
@@ -162,9 +216,9 @@ function extractPatch(response: string): Record<string, unknown> | null {
|
|
|
162
216
|
const match = response.match(/\`\`\`json\\s*\\n?([\\s\\S]*?)\\n?\\s*\`\`\`/);
|
|
163
217
|
if (!match) return null;
|
|
164
218
|
try {
|
|
165
|
-
const parsed = JSON.parse(match[1]);
|
|
166
|
-
if (parsed
|
|
167
|
-
return parsed
|
|
219
|
+
const parsed: unknown = JSON.parse(match[1]);
|
|
220
|
+
if (isPlainObject(parsed) && isPlainObject(parsed["state_patch"])) {
|
|
221
|
+
return parsed["state_patch"];
|
|
168
222
|
}
|
|
169
223
|
} catch {
|
|
170
224
|
// Malformed JSON — ignore.
|
|
@@ -172,92 +226,76 @@ function extractPatch(response: string): Record<string, unknown> | null {
|
|
|
172
226
|
return null;
|
|
173
227
|
}
|
|
174
228
|
|
|
175
|
-
|
|
229
|
+
const SkillStatePlugin: Plugin = async () => {
|
|
176
230
|
return {
|
|
177
231
|
// ── O(1) history trimming ──────────────────────────────────────────
|
|
178
232
|
// Filters messages BEFORE each LLM call: keeps all system messages
|
|
179
233
|
// plus the last ${maxHistory} non-system messages, then injects a
|
|
180
|
-
// synthetic
|
|
181
|
-
//
|
|
182
|
-
"experimental.chat.messages.transform": async (
|
|
234
|
+
// synthetic state element. Old messages are DROPPED from the prompt,
|
|
235
|
+
// not just hidden.
|
|
236
|
+
"experimental.chat.messages.transform": async (_input, output) => {
|
|
183
237
|
const state = readSkillState();
|
|
184
|
-
const messages = output.messages
|
|
185
|
-
const systemMessages = messages.filter((m) => m.role === "system");
|
|
186
|
-
const
|
|
187
|
-
|
|
238
|
+
const messages = output.messages;
|
|
239
|
+
const systemMessages = messages.filter((m) => m.info.role === "system");
|
|
240
|
+
const trimmed = messages
|
|
241
|
+
.filter((m) => m.info.role !== "system")
|
|
242
|
+
.slice(-MAX_HISTORY);
|
|
188
243
|
|
|
189
244
|
const stateMessage = {
|
|
190
|
-
|
|
191
|
-
|
|
245
|
+
info: {
|
|
246
|
+
id: "skillstate-state-inject",
|
|
247
|
+
sessionID: "skillstate",
|
|
248
|
+
role: "user",
|
|
249
|
+
time: { created: 0 },
|
|
250
|
+
agent: "skillstate",
|
|
251
|
+
model: { providerID: "skillstate", modelID: "skillstate" },
|
|
252
|
+
},
|
|
253
|
+
parts: [
|
|
254
|
+
{
|
|
255
|
+
id: "skillstate-state-inject-text",
|
|
256
|
+
sessionID: "skillstate",
|
|
257
|
+
messageID: "skillstate-state-inject",
|
|
258
|
+
type: "text",
|
|
259
|
+
synthetic: true,
|
|
260
|
+
text: "Current skill state (JSON): " + JSON.stringify(state),
|
|
261
|
+
},
|
|
262
|
+
],
|
|
192
263
|
};
|
|
193
264
|
|
|
194
|
-
|
|
195
|
-
|
|
265
|
+
// The pipeline holds the original array reference — mutate in place.
|
|
266
|
+
const kept = [...systemMessages, ...trimmed, stateMessage];
|
|
267
|
+
messages.length = 0;
|
|
268
|
+
messages.push(...kept);
|
|
196
269
|
},
|
|
197
270
|
|
|
198
271
|
// ── Compaction context injection ───────────────────────────────────
|
|
199
272
|
// Before compaction, inject the current state into the context so the
|
|
200
273
|
// compaction summary preserves state even after history is compressed.
|
|
201
|
-
"experimental.session.compacting": async (
|
|
274
|
+
"experimental.session.compacting": async (_input, output) => {
|
|
202
275
|
const state = readSkillState();
|
|
203
276
|
const stateContext = "Skillstate: " + JSON.stringify(state);
|
|
204
277
|
if (!Array.isArray(output.context)) {
|
|
205
278
|
output.context = [];
|
|
206
279
|
}
|
|
207
280
|
output.context.push(stateContext);
|
|
208
|
-
return output;
|
|
209
281
|
},
|
|
210
282
|
|
|
211
283
|
// ── State persistence from LLM responses ───────────────────────────
|
|
212
|
-
// After tool execution, extract state_patch from the
|
|
213
|
-
//
|
|
214
|
-
"tool.execute.after": async (
|
|
215
|
-
const response = output
|
|
284
|
+
// After tool execution, extract state_patch from the tool response
|
|
285
|
+
// (output.output), merge it, and save to disk.
|
|
286
|
+
"tool.execute.after": async (_input, output) => {
|
|
287
|
+
const response = output.output ?? "";
|
|
216
288
|
if (typeof response === "string") {
|
|
217
289
|
const patch = extractPatch(response);
|
|
218
290
|
if (patch) {
|
|
219
|
-
|
|
220
|
-
const merged = mergePatch(current, patch);
|
|
221
|
-
saveSkillState(merged);
|
|
291
|
+
saveSkillState(STATE_PATH, mergePatch(readSkillState(), patch));
|
|
222
292
|
}
|
|
223
293
|
}
|
|
224
|
-
return output;
|
|
225
294
|
},
|
|
226
295
|
};
|
|
227
296
|
};
|
|
228
297
|
|
|
229
298
|
export default SkillStatePlugin;
|
|
230
299
|
`;
|
|
231
|
-
}
|
|
232
|
-
/**
|
|
233
|
-
* @non-paper additive helper: generate the plugin and persist it via
|
|
234
|
-
* `atomicWriteFile` (tmp + fsync + rename). Both the destination and the
|
|
235
|
-
* embedded state path accept raw strings (legacy behavior) or
|
|
236
|
-
* `{ root, name }` refs confined by `resolveStatePath`. Returns the
|
|
237
|
-
* absolute destination path.
|
|
238
|
-
*/
|
|
239
|
-
async savePluginCode(target, statePath, options) {
|
|
240
|
-
const dest = typeof target === 'string'
|
|
241
|
-
? target
|
|
242
|
-
: resolveStatePath(target.root, target.name);
|
|
243
|
-
const resolvedState = typeof statePath === 'string'
|
|
244
|
-
? statePath
|
|
245
|
-
: resolveStatePath(statePath.root, statePath.name);
|
|
246
|
-
const plugin = this.generatePluginCode(resolvedState, options);
|
|
247
|
-
await atomicWriteFile(dest, plugin);
|
|
248
|
-
return dest;
|
|
249
|
-
}
|
|
250
|
-
/* ------------------------------------------------------------------ */
|
|
251
|
-
/* Internal helpers */
|
|
252
|
-
/* ------------------------------------------------------------------ */
|
|
253
|
-
/**
|
|
254
|
-
* Describe the schema fields for inclusion in prompts.
|
|
255
|
-
*/
|
|
256
|
-
describeSchema(schema) {
|
|
257
|
-
const fields = Object.entries(schema)
|
|
258
|
-
.map(([name, field]) => `- ${name} (${field.type}): ${field.description ?? 'no description'}`)
|
|
259
|
-
.join('\n');
|
|
260
|
-
return `## Schema\n${fields}`;
|
|
261
|
-
}
|
|
262
300
|
}
|
|
263
301
|
//# sourceMappingURL=opencode-adapter.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"opencode-adapter.js","sourceRoot":"","sources":["../src/opencode-adapter.ts"],"names":[],"mappings":"AAqBA,OAAO,EAAE,iBAAiB,EAAE,MAAM,kBAAkB,CAAC;AACrD,OAAO,EACL,eAAe,EACf,gBAAgB,GACjB,MAAM,kBAAkB,CAAC;
|
|
1
|
+
{"version":3,"file":"opencode-adapter.js","sourceRoot":"","sources":["../src/opencode-adapter.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Minimal local types for OpenCode plugin hooks — the `@opencode-ai/plugin`
|
|
3
|
+
* shape, verified against opencode 1.17 (PluginInput/Hooks). Declared locally
|
|
4
|
+
* to keep `@skillstate/opencode` zero-dep; the host supplies the real types
|
|
5
|
+
* at runtime.
|
|
6
|
+
*/
|
|
7
|
+
/** Message roles seen by `experimental.chat.messages.transform`. */
|
|
8
|
+
export type OpenCodeMessageRole = 'user' | 'assistant' | 'system';
|
|
9
|
+
/**
|
|
10
|
+
* Message envelope: the transform pipeline hands each message as
|
|
11
|
+
* `{ info, parts }` — the role lives on `info.role`.
|
|
12
|
+
*/
|
|
13
|
+
export interface OpenCodeMessage {
|
|
14
|
+
info: {
|
|
15
|
+
role: OpenCodeMessageRole | string;
|
|
16
|
+
[key: string]: unknown;
|
|
17
|
+
};
|
|
18
|
+
parts: Array<{
|
|
19
|
+
type: string;
|
|
20
|
+
text?: string;
|
|
21
|
+
[key: string]: unknown;
|
|
22
|
+
}>;
|
|
23
|
+
}
|
|
24
|
+
/** Output payload of `experimental.chat.messages.transform`. */
|
|
25
|
+
export interface MessagesTransformOutput {
|
|
26
|
+
/** Mutated IN PLACE — the pipeline keeps the original array reference. */
|
|
27
|
+
messages: OpenCodeMessage[];
|
|
28
|
+
}
|
|
29
|
+
/** Output payload of `experimental.session.compacting`. */
|
|
30
|
+
export interface SessionCompactingOutput {
|
|
31
|
+
context: string[];
|
|
32
|
+
prompt?: string;
|
|
33
|
+
}
|
|
34
|
+
/** Output payload of `tool.execute.after`. */
|
|
35
|
+
export interface ToolExecuteAfterOutput {
|
|
36
|
+
title?: string;
|
|
37
|
+
/** The tool's string response (state_patch extraction source). */
|
|
38
|
+
output: unknown;
|
|
39
|
+
metadata?: unknown;
|
|
40
|
+
}
|
|
41
|
+
/** OpenCode hooks used by the skillstate plugin. */
|
|
42
|
+
export interface SkillStateHooks {
|
|
43
|
+
'experimental.chat.messages.transform'?: (input: Record<string, never>, output: MessagesTransformOutput) => Promise<void>;
|
|
44
|
+
'experimental.session.compacting'?: (input: {
|
|
45
|
+
sessionID: string;
|
|
46
|
+
}, output: SessionCompactingOutput) => Promise<void>;
|
|
47
|
+
'tool.execute.after'?: (input: {
|
|
48
|
+
tool: string;
|
|
49
|
+
sessionID: string;
|
|
50
|
+
callID: string;
|
|
51
|
+
args: unknown;
|
|
52
|
+
}, output: ToolExecuteAfterOutput) => Promise<void>;
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* OpenCode plugin: an async factory receiving the host input and returning
|
|
56
|
+
* the hooks object (`Plugin` from `@opencode-ai/plugin`).
|
|
57
|
+
*/
|
|
58
|
+
export type SkillStatePlugin = (input: {
|
|
59
|
+
project?: unknown;
|
|
60
|
+
client?: unknown;
|
|
61
|
+
$?: unknown;
|
|
62
|
+
[key: string]: unknown;
|
|
63
|
+
}) => Promise<SkillStateHooks>;
|
|
64
|
+
//# sourceMappingURL=plugin-types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"plugin-types.d.ts","sourceRoot":"","sources":["../src/plugin-types.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,oEAAoE;AACpE,MAAM,MAAM,mBAAmB,GAAG,MAAM,GAAG,WAAW,GAAG,QAAQ,CAAC;AAElE;;;GAGG;AACH,MAAM,WAAW,eAAe;IAC9B,IAAI,EAAE;QAAE,IAAI,EAAE,mBAAmB,GAAG,MAAM,CAAC;QAAC,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAA;KAAE,CAAC;IACrE,KAAK,EAAE,KAAK,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,IAAI,CAAC,EAAE,MAAM,CAAC;QAAC,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAA;KAAE,CAAC,CAAC;CACvE;AAED,gEAAgE;AAChE,MAAM,WAAW,uBAAuB;IACtC,0EAA0E;IAC1E,QAAQ,EAAE,eAAe,EAAE,CAAC;CAC7B;AAED,2DAA2D;AAC3D,MAAM,WAAW,uBAAuB;IACtC,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,8CAA8C;AAC9C,MAAM,WAAW,sBAAsB;IACrC,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,kEAAkE;IAClE,MAAM,EAAE,OAAO,CAAC;IAChB,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB;AAED,oDAAoD;AACpD,MAAM,WAAW,eAAe;IAC9B,sCAAsC,CAAC,EAAE,CACvC,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,KAAK,CAAC,EAC5B,MAAM,EAAE,uBAAuB,KAC5B,OAAO,CAAC,IAAI,CAAC,CAAC;IACnB,iCAAiC,CAAC,EAAE,CAClC,KAAK,EAAE;QAAE,SAAS,EAAE,MAAM,CAAA;KAAE,EAC5B,MAAM,EAAE,uBAAuB,KAC5B,OAAO,CAAC,IAAI,CAAC,CAAC;IACnB,oBAAoB,CAAC,EAAE,CACrB,KAAK,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,SAAS,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,OAAO,CAAA;KAAE,EACzE,MAAM,EAAE,sBAAsB,KAC3B,OAAO,CAAC,IAAI,CAAC,CAAC;CACpB;AAED;;;GAGG;AACH,MAAM,MAAM,gBAAgB,GAAG,CAAC,KAAK,EAAE;IACrC,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,CAAC,CAAC,EAAE,OAAO,CAAC;IACZ,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;CACxB,KAAK,OAAO,CAAC,eAAe,CAAC,CAAC"}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Minimal local types for OpenCode plugin hooks — the `@opencode-ai/plugin`
|
|
3
|
+
* shape, verified against opencode 1.17 (PluginInput/Hooks). Declared locally
|
|
4
|
+
* to keep `@skillstate/opencode` zero-dep; the host supplies the real types
|
|
5
|
+
* at runtime.
|
|
6
|
+
*/
|
|
7
|
+
export {};
|
|
8
|
+
//# sourceMappingURL=plugin-types.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"plugin-types.js","sourceRoot":"","sources":["../src/plugin-types.ts"],"names":[],"mappings":"AAAA;;;;;GAKG"}
|
package/dist/plugin.d.ts
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import type { SkillStatePlugin } from './plugin-types.js';
|
|
2
|
+
export * from './plugin-types.js';
|
|
3
|
+
/** Options for {@link createSkillStatePlugin}. */
|
|
4
|
+
export interface SkillStatePluginOptions {
|
|
5
|
+
/** Absolute path of the persisted state file. */
|
|
6
|
+
statePath: string;
|
|
7
|
+
/** Non-system messages kept in the prompt (default 3). */
|
|
8
|
+
maxHistoryMessages?: number;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Read the state file. Missing or corrupt files yield `{}` (best-effort).
|
|
12
|
+
* The on-disk envelope is `{ version: 1, state }` (migrations-compatible);
|
|
13
|
+
* a bare object is tolerated and treated as the state itself.
|
|
14
|
+
*/
|
|
15
|
+
export declare function readSkillState(statePath: string): Record<string, unknown>;
|
|
16
|
+
/**
|
|
17
|
+
* Persist the state file (best-effort: read-only environments are ignored).
|
|
18
|
+
* Writes the `{ version: 1, state }` envelope so `migrate()`/runtime resume
|
|
19
|
+
* read the same file.
|
|
20
|
+
*/
|
|
21
|
+
export declare function saveSkillState(statePath: string, state: Record<string, unknown>): void;
|
|
22
|
+
/**
|
|
23
|
+
* Paper ⊕ merge: `null` deletes a key, nested plain objects merge
|
|
24
|
+
* recursively, everything else replaces.
|
|
25
|
+
*/
|
|
26
|
+
export declare function mergePatch(base: Record<string, unknown>, patch: Record<string, unknown>): Record<string, unknown>;
|
|
27
|
+
/**
|
|
28
|
+
* Extract the `state_patch` object from an LLM response's fenced ```json
|
|
29
|
+
* block; `null` when there is no block, it is malformed, or it carries no
|
|
30
|
+
* object-shaped `state_patch`.
|
|
31
|
+
*/
|
|
32
|
+
export declare function extractPatch(response: string): Record<string, unknown> | null;
|
|
33
|
+
/**
|
|
34
|
+
* Build the OpenCode plugin function with the same behavior for every host
|
|
35
|
+
* entry point (thin generated loaders, direct imports).
|
|
36
|
+
*/
|
|
37
|
+
export declare function createSkillStatePlugin(options: SkillStatePluginOptions): SkillStatePlugin;
|
|
38
|
+
//# sourceMappingURL=plugin.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"plugin.d.ts","sourceRoot":"","sources":["../src/plugin.ts"],"names":[],"mappings":"AAkBA,OAAO,KAAK,EAGV,gBAAgB,EACjB,MAAM,mBAAmB,CAAC;AAE3B,cAAc,mBAAmB,CAAC;AAElC,kDAAkD;AAClD,MAAM,WAAW,uBAAuB;IACtC,iDAAiD;IACjD,SAAS,EAAE,MAAM,CAAC;IAClB,0DAA0D;IAC1D,kBAAkB,CAAC,EAAE,MAAM,CAAC;CAC7B;AAOD;;;;GAIG;AACH,wBAAgB,cAAc,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAqBzE;AAED;;;;GAIG;AACH,wBAAgB,cAAc,CAAC,SAAS,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAMtF;AAED;;;GAGG;AACH,wBAAgB,UAAU,CACxB,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC7B,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAC7B,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAazB;AAED;;;;GAIG;AACH,wBAAgB,YAAY,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAY7E;AAKD;;;GAGG;AACH,wBAAgB,sBAAsB,CAAC,OAAO,EAAE,uBAAuB,GAAG,gBAAgB,CA+EzF"}
|
package/dist/plugin.js
ADDED
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Static OpenCode plugin — the SINGLE SOURCE OF TRUTH for the skillstate
|
|
3
|
+
* host integration. `OpenCodeAdapter.generatePluginCode` (thin mode, the
|
|
4
|
+
* default) emits a loader that imports `createSkillStatePlugin` from this
|
|
5
|
+
* module; the standalone template is only an escape hatch for environments
|
|
6
|
+
* without npm resolution of `@skillstate/opencode`.
|
|
7
|
+
*
|
|
8
|
+
* Hooks (opencode 1.17 contract, verified on host):
|
|
9
|
+
* - `experimental.chat.messages.transform` — entries are `{ info, parts }`
|
|
10
|
+
* envelopes (role on `info.role`); the pipeline keeps the ORIGINAL array
|
|
11
|
+
* reference, so trimming mutates in place; the state is injected as a
|
|
12
|
+
* synthetic `{ info, parts }` element. Real O(1) prompt footprint.
|
|
13
|
+
* - `experimental.session.compacting` — pushes the state into
|
|
14
|
+
* `output.context` so the compaction summary preserves it.
|
|
15
|
+
* - `tool.execute.after` — the tool response is `output.output`; a fenced
|
|
16
|
+
* ```json `state_patch` block is merged (paper ⊕: null deletes) and saved.
|
|
17
|
+
*/
|
|
18
|
+
import * as fs from 'node:fs';
|
|
19
|
+
export * from './plugin-types.js';
|
|
20
|
+
/** True for plain (non-null, non-array) objects. */
|
|
21
|
+
function isPlainObject(value) {
|
|
22
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Read the state file. Missing or corrupt files yield `{}` (best-effort).
|
|
26
|
+
* The on-disk envelope is `{ version: 1, state }` (migrations-compatible);
|
|
27
|
+
* a bare object is tolerated and treated as the state itself.
|
|
28
|
+
*/
|
|
29
|
+
export function readSkillState(statePath) {
|
|
30
|
+
try {
|
|
31
|
+
if (fs.existsSync(statePath)) {
|
|
32
|
+
const parsed = JSON.parse(fs.readFileSync(statePath, 'utf-8'));
|
|
33
|
+
if (typeof parsed === 'object' &&
|
|
34
|
+
parsed !== null &&
|
|
35
|
+
'state' in parsed &&
|
|
36
|
+
typeof parsed['state'] === 'object' &&
|
|
37
|
+
parsed['state'] !== null) {
|
|
38
|
+
return parsed['state'];
|
|
39
|
+
}
|
|
40
|
+
if (typeof parsed === 'object' && parsed !== null) {
|
|
41
|
+
return parsed;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
catch {
|
|
46
|
+
// Corrupt or unreadable state file — fall back to empty state.
|
|
47
|
+
}
|
|
48
|
+
return {};
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Persist the state file (best-effort: read-only environments are ignored).
|
|
52
|
+
* Writes the `{ version: 1, state }` envelope so `migrate()`/runtime resume
|
|
53
|
+
* read the same file.
|
|
54
|
+
*/
|
|
55
|
+
export function saveSkillState(statePath, state) {
|
|
56
|
+
try {
|
|
57
|
+
fs.writeFileSync(statePath, `${JSON.stringify({ version: 1, state }, null, 2)}\n`);
|
|
58
|
+
}
|
|
59
|
+
catch {
|
|
60
|
+
// Best-effort: read-only environments or permission issues.
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Paper ⊕ merge: `null` deletes a key, nested plain objects merge
|
|
65
|
+
* recursively, everything else replaces.
|
|
66
|
+
*/
|
|
67
|
+
export function mergePatch(base, patch) {
|
|
68
|
+
const result = { ...base };
|
|
69
|
+
for (const key of Object.keys(patch)) {
|
|
70
|
+
const value = patch[key];
|
|
71
|
+
if (value === null) {
|
|
72
|
+
delete result[key];
|
|
73
|
+
}
|
|
74
|
+
else if (isPlainObject(value) && isPlainObject(result[key])) {
|
|
75
|
+
result[key] = mergePatch(result[key], value);
|
|
76
|
+
}
|
|
77
|
+
else {
|
|
78
|
+
result[key] = value;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
return result;
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Extract the `state_patch` object from an LLM response's fenced ```json
|
|
85
|
+
* block; `null` when there is no block, it is malformed, or it carries no
|
|
86
|
+
* object-shaped `state_patch`.
|
|
87
|
+
*/
|
|
88
|
+
export function extractPatch(response) {
|
|
89
|
+
const match = response.match(/```json\s*\n?([\s\S]*?)\n?\s*```/);
|
|
90
|
+
if (!match)
|
|
91
|
+
return null;
|
|
92
|
+
try {
|
|
93
|
+
const parsed = JSON.parse(match[1]);
|
|
94
|
+
if (isPlainObject(parsed) && isPlainObject(parsed['state_patch'])) {
|
|
95
|
+
return parsed['state_patch'];
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
catch {
|
|
99
|
+
// Malformed JSON — ignore.
|
|
100
|
+
}
|
|
101
|
+
return null;
|
|
102
|
+
}
|
|
103
|
+
/** Synthetic message ids for the injected state carrier. */
|
|
104
|
+
const STATE_MESSAGE_ID = 'skillstate-state-inject';
|
|
105
|
+
/**
|
|
106
|
+
* Build the OpenCode plugin function with the same behavior for every host
|
|
107
|
+
* entry point (thin generated loaders, direct imports).
|
|
108
|
+
*/
|
|
109
|
+
export function createSkillStatePlugin(options) {
|
|
110
|
+
const statePath = options.statePath;
|
|
111
|
+
const maxHistory = options.maxHistoryMessages ?? 3;
|
|
112
|
+
return async () => {
|
|
113
|
+
return {
|
|
114
|
+
// ── O(1) history trimming ──────────────────────────────────────────
|
|
115
|
+
// Filters messages BEFORE each LLM call: keeps all system messages
|
|
116
|
+
// plus the last `maxHistory` non-system messages, then injects a
|
|
117
|
+
// synthetic state element. Old messages are DROPPED from the prompt,
|
|
118
|
+
// not just hidden.
|
|
119
|
+
'experimental.chat.messages.transform': async (_input, output) => {
|
|
120
|
+
const state = readSkillState(statePath);
|
|
121
|
+
const messages = output.messages;
|
|
122
|
+
const systemMessages = messages.filter((m) => m.info.role === 'system');
|
|
123
|
+
const trimmed = messages
|
|
124
|
+
.filter((m) => m.info.role !== 'system')
|
|
125
|
+
.slice(-maxHistory);
|
|
126
|
+
// Synthetic state carrier — a `{ info, parts }` envelope whose text
|
|
127
|
+
// part carries the current state JSON.
|
|
128
|
+
const stateMessage = {
|
|
129
|
+
info: {
|
|
130
|
+
id: STATE_MESSAGE_ID,
|
|
131
|
+
sessionID: 'skillstate',
|
|
132
|
+
role: 'user',
|
|
133
|
+
time: { created: 0 },
|
|
134
|
+
agent: 'skillstate',
|
|
135
|
+
model: { providerID: 'skillstate', modelID: 'skillstate' },
|
|
136
|
+
},
|
|
137
|
+
parts: [
|
|
138
|
+
{
|
|
139
|
+
id: `${STATE_MESSAGE_ID}-text`,
|
|
140
|
+
sessionID: 'skillstate',
|
|
141
|
+
messageID: STATE_MESSAGE_ID,
|
|
142
|
+
type: 'text',
|
|
143
|
+
synthetic: true,
|
|
144
|
+
text: `Current skill state (JSON): ${JSON.stringify(state)}`,
|
|
145
|
+
},
|
|
146
|
+
],
|
|
147
|
+
};
|
|
148
|
+
// The pipeline holds the original array reference — mutate in place
|
|
149
|
+
// (reassigning `output.messages` would not reach the LLM call).
|
|
150
|
+
const kept = [...systemMessages, ...trimmed, stateMessage];
|
|
151
|
+
messages.length = 0;
|
|
152
|
+
messages.push(...kept);
|
|
153
|
+
},
|
|
154
|
+
// ── Compaction context injection ───────────────────────────────────
|
|
155
|
+
// Before compaction, inject the current state into the context so the
|
|
156
|
+
// compaction summary preserves state even after history is compressed.
|
|
157
|
+
'experimental.session.compacting': async (_input, output) => {
|
|
158
|
+
const state = readSkillState(statePath);
|
|
159
|
+
if (!Array.isArray(output.context)) {
|
|
160
|
+
output.context = [];
|
|
161
|
+
}
|
|
162
|
+
output.context.push(`Skillstate: ${JSON.stringify(state)}`);
|
|
163
|
+
},
|
|
164
|
+
// ── State persistence from LLM responses ───────────────────────────
|
|
165
|
+
// After tool execution, extract state_patch from the tool response
|
|
166
|
+
// (output.output), merge it, and save to disk.
|
|
167
|
+
'tool.execute.after': async (_input, output) => {
|
|
168
|
+
const response = output.output ?? '';
|
|
169
|
+
if (typeof response !== 'string')
|
|
170
|
+
return;
|
|
171
|
+
const patch = extractPatch(response);
|
|
172
|
+
if (patch) {
|
|
173
|
+
saveSkillState(statePath, mergePatch(readSkillState(statePath), patch));
|
|
174
|
+
}
|
|
175
|
+
},
|
|
176
|
+
};
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
//# sourceMappingURL=plugin.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"plugin.js","sourceRoot":"","sources":["../src/plugin.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@skillstate/opencode",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.4",
|
|
4
4
|
"description": "OpenCode platform adapter for the skillstate runtime.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -10,6 +10,10 @@
|
|
|
10
10
|
"types": "./dist/index.d.ts",
|
|
11
11
|
"default": "./dist/index.js"
|
|
12
12
|
},
|
|
13
|
+
"./plugin": {
|
|
14
|
+
"types": "./dist/plugin.d.ts",
|
|
15
|
+
"default": "./dist/plugin.js"
|
|
16
|
+
},
|
|
13
17
|
"./package.json": "./package.json"
|
|
14
18
|
},
|
|
15
19
|
"files": [
|