@skillstate/opencode 2.2.1 → 3.0.0

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/dist/plugin.d.ts CHANGED
@@ -1,77 +1,87 @@
1
- import { mergePatch, resolveHostStateForCwd } from '@skillstate/core';
2
- import type { OpenCodeMessage, SkillStatePlugin } from './plugin-types.js';
3
- export * from './plugin-types.js';
4
1
  /**
5
- * Resolve the per-project state file for a session working directory
6
- * (`cwd` of the current opencode session) — the core single source of
7
- * truth (`resolveHostStateForCwd`): `<cwd>/.skillstate/skillstate.json`,
8
- * or the global bucket `<home>/.skillstate/global/skillstate.json` when
9
- * cwd equals home. A non-empty `agentId` scopes the file under
10
- * `<bucket>/agents/<agentId>/skillstate.json`. Pure path arithmetic via
11
- * `path.resolve`, no filesystem access.
12
- */
13
- export { resolveHostStateForCwd as resolveStatePathForCwd };
14
- export { mergePatch };
15
- /** Agent scope used by the plugin when no session id is available. */
16
- export declare const PLUGIN_DEFAULT_AGENT_ID = "default";
17
- /** Options for {@link createSkillStatePlugin}. */
18
- export interface SkillStatePluginOptions {
19
- /** Non-system messages kept in the prompt (default 3). */
20
- maxHistoryMessages?: number;
21
- }
22
- /**
23
- * Read the state file. Missing or corrupt files yield `{}` (best-effort).
24
- * The on-disk envelope is `{ version: 1, state }` (migrations-compatible);
25
- * a bare object is tolerated and treated as the state itself. Thin fs
26
- * adapter over the core hook-runtime {@link readStateEnvelope}.
27
- */
28
- export declare function readSkillState(statePath: string): Record<string, unknown>;
29
- /**
30
- * Persist the state file (best-effort: read-only environments are ignored).
31
- * Creates the parent directory when missing (the per-project resolver may
32
- * target a fresh `<cwd>/.skillstate/agents/<id>/`). Writes the
33
- * `{ version: 1, state }` envelope so `migrate()`/runtime resume read the
34
- * same file — via the core hook-runtime {@link saveStateEnvelope} — under
35
- * the cross-process sync lock {@link lockStateWrite} (2-3 parallel agent
36
- * processes never interleave state writes).
37
- */
38
- export declare function saveSkillState(statePath: string, state: Record<string, unknown>): void;
39
- /**
40
- * Atomic READ-MERGE-WRITE of one `state_patch` (paper ⊕: null deletes):
41
- * the whole critical section runs inside {@link lockStateWrite}, so two
42
- * concurrent writers apply BOTH patches instead of racing between the
43
- * read and the write. Best-effort: lock contention or unwritable state
44
- * files are swallowed — the tool flow never breaks.
45
- */
46
- export declare function mergeSkillState(statePath: string, patch: Record<string, unknown>): Record<string, unknown>;
47
- /**
48
- * Extract the `state_patch` object from an LLM response's fenced ```json
49
- * block; `null` when there is no block, it is malformed, or it carries no
50
- * object-shaped `state_patch`. Thin adapter over the core hook-runtime
51
- * {@link findFencedPatch} (the invalid/truncated outcomes collapse to
52
- * `null`, preserving the legacy boolean contract).
53
- */
54
- export declare function extractPatch(response: string): Record<string, unknown> | null;
55
- /**
56
- * Agent id for an opencode hook call: the 8-char session prefix from
57
- * `input.sessionID` when the hook carries it, else the first non-synthetic
58
- * message's `info.sessionID` (the transform hook input is empty — the
59
- * session lives on the message envelopes), else `'default'` (the plugin
60
- * trims the history of one session context).
2
+ * `@skillstate/opencode` — the OpenCode **v2** plugin.
3
+ *
4
+ * ── What this replaces ───────────────────────────────────────────────────
5
+ *
6
+ * The v1 integration rewrote the conversation on every model request. It
7
+ * kept the system messages and the last three non-system messages, dropped
8
+ * everything else from `output.messages`, and appended a synthetic
9
+ * `role: "user"` message containing the raw state JSON. The reported
10
+ * failure was that the agent stopped doing the user's task and started
11
+ * emitting state JSON instead.
12
+ *
13
+ * Both halves of that were destructive, and neither was a model quirk:
14
+ *
15
+ * 1. The injected message landed LAST, so for the model it was the current
16
+ * instruction — it displaced the user's actual request.
17
+ * 2. `slice(-3)` deleted the task statement, the tool results and the
18
+ * errors the agent had just been handed. It was reasoning about work it
19
+ * could no longer see.
20
+ *
21
+ * The MCP server made it worse: `spec.get` returned a procedural spec whose
22
+ * default was `INTERCODE_CTF_SPEC`, whose instructions read "You are an
23
+ * autonomous CTF agent ... hidden flag somewhere on its filesystem". A
24
+ * model told to look for a flag looks for a flag. (Fixed: the default is now
25
+ * the neutral `GENERIC_PROCEDURE_SPEC`, and its instructions describe the
26
+ * storage format instead of prescribing a way of working.)
27
+ *
28
+ * ── The v2 design ────────────────────────────────────────────────────────
29
+ *
30
+ * Three rules, each enforced by a test:
31
+ *
32
+ * - **Never mutate `event.messages`.** The plugin contributes one additive
33
+ * fragment to `event.system` and leaves the transcript alone. See
34
+ * `tests/opencode/context-integrity.test.ts`.
35
+ * - **Never inject behavioural instructions.** The system fragment
36
+ * describes what the notes are and when to use them; it contains no
37
+ * "you must", no "always", and no output format. See
38
+ * `system-hint.ts`.
39
+ * - **Inert until used.** A project with no state file gets no system
40
+ * fragment at all and behaves exactly like vanilla OpenCode. No files are
41
+ * created by loading the plugin.
42
+ *
43
+ * ── Native tools AND the MCP server, on purpose ──────────────────────────
44
+ *
45
+ * This package does not replace `@skillstate/mcp`; it sits beside it.
46
+ *
47
+ * - The native tools ({@link registerTools}) are the fast path inside
48
+ * opencode: a typed schema, structured output, no JSON-RPC round-trip and
49
+ * no untyped text result.
50
+ * - The MCP server is the portable path. It is what every other
51
+ * MCP-capable host reads, and the only way to reach this state from a
52
+ * client that is not opencode.
53
+ *
54
+ * Both address the same `<project>/.skillstate/skillstate.json`, so they
55
+ * cannot disagree about what is saved. `skillstate init` registers both.
56
+ *
57
+ * The reason v1 needed the MCP server is gone: an opencode v1 plugin could
58
+ * not contribute first-class tools at all.
59
+ *
60
+ * Load it from `opencode.json(c)`:
61
+ *
62
+ * ```json
63
+ * { "plugins": ["@skillstate/opencode"] }
64
+ * ```
61
65
  */
62
- export declare function pluginAgentId(input: {
63
- sessionID?: unknown;
64
- }, messages?: OpenCodeMessage[]): string;
66
+ import { Plugin } from '@opencode/plugin';
67
+ /** Stable plugin id — scopes plugin storage and identifies it in `/api/plugin`. */
68
+ export declare const PLUGIN_ID = "skillstate";
65
69
  /**
66
- * Build the OpenCode plugin function with the same behavior for every host
67
- * entry point (thin generated loaders, direct imports).
68
- *
69
- * State resolution is ALWAYS per-project: the state file path is computed
70
- * from the session cwd on EVERY hook call via
71
- * `resolveStatePathForCwd(process.cwd(), os.homedir(), agentId)` — each
72
- * project gets its own `<cwd>/.skillstate/`, each session (sub-agent) its
73
- * isolated `agents/<session>/` copy, and a session launched from `$HOME`
74
- * uses the global bucket.
70
+ * The plugin definition.
71
+ *
72
+ * `setup` wires three things and returns a cleanup function:
73
+ *
74
+ * - a {@link SessionRegistry}, fed by the server event stream, so a
75
+ * sub-agent session is recognised and given its own state file;
76
+ * - a {@link ProjectStateStore} rooted at the plugin's own project
77
+ * location, so two checkouts served by one OpenCode server never share
78
+ * state;
79
+ * - native tools plus a single additive `context` hook.
80
+ *
81
+ * The event subscription is the only resource the plugin owns, so the
82
+ * returned cleanup aborts it. Hook and tool registrations are disposed by
83
+ * OpenCode when the plugin unloads.
75
84
  */
76
- export declare function createSkillStatePlugin(options?: SkillStatePluginOptions): SkillStatePlugin;
85
+ export declare const SkillStatePlugin: Plugin.Plugin;
86
+ export default SkillStatePlugin;
77
87
  //# sourceMappingURL=plugin.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"plugin.d.ts","sourceRoot":"","sources":["../src/plugin.ts"],"names":[],"mappings":"AA+BA,OAAO,EAGL,UAAU,EAGV,sBAAsB,EAEvB,MAAM,kBAAkB,CAAC;AAC1B,OAAO,KAAK,EACV,eAAe,EAEf,gBAAgB,EACjB,MAAM,mBAAmB,CAAC;AAE3B,cAAc,mBAAmB,CAAC;AAElC;;;;;;;;GAQG;AACH,OAAO,EAAE,sBAAsB,IAAI,sBAAsB,EAAE,CAAC;AAE5D,OAAO,EAAE,UAAU,EAAE,CAAC;AAEtB,sEAAsE;AACtE,eAAO,MAAM,uBAAuB,YAAY,CAAC;AAEjD,kDAAkD;AAClD,MAAM,WAAW,uBAAuB;IACtC,0DAA0D;IAC1D,kBAAkB,CAAC,EAAE,MAAM,CAAC;CAC7B;AAED;;;;;GAKG;AACH,wBAAgB,cAAc,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAEzE;AAED;;;;;;;;GAQG;AACH,wBAAgB,cAAc,CAAC,SAAS,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAWtF;AAED;;;;;;GAMG;AACH,wBAAgB,eAAe,CAC7B,SAAS,EAAE,MAAM,EACjB,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAC7B,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAYzB;AAED;;;;;;GAMG;AACH,wBAAgB,YAAY,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAG7E;AAED;;;;;;GAMG;AACH,wBAAgB,aAAa,CAC3B,KAAK,EAAE;IAAE,SAAS,CAAC,EAAE,OAAO,CAAA;CAAE,EAC9B,QAAQ,CAAC,EAAE,eAAe,EAAE,GAC3B,MAAM,CAWR;AAKD;;;;;;;;;;GAUG;AACH,wBAAgB,sBAAsB,CAAC,OAAO,GAAE,uBAA4B,GAAG,gBAAgB,CAiF9F"}
1
+ {"version":3,"file":"plugin.d.ts","sourceRoot":"","sources":["../src/plugin.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgEG;AAEH,OAAO,EAAE,MAAM,EAAE,MAAM,kBAAkB,CAAC;AAO1C,mFAAmF;AACnF,eAAO,MAAM,SAAS,eAAe,CAAC;AAEtC;;;;;;;;;;;;;;;GAeG;AACH,eAAO,MAAM,gBAAgB,eAyD3B,CAAC;eAEY,gBAAgB"}
package/dist/plugin.js CHANGED
@@ -1,210 +1,147 @@
1
1
  /**
2
- * Static OpenCode plugin — the SINGLE SOURCE OF TRUTH for the skillstate
3
- * host integration. `OpenCodeAdapter.generatePluginCode` emits a thin loader
4
- * that imports `createSkillStatePlugin` from this module; the per-project
5
- * state resolution lives in `@skillstate/core`
6
- * (`resolveHostStateForCwd`, re-exported here) and the hook logic
7
- * (envelope read/write, ⊕ merge, patch extraction) in the core
8
- * hook-runtime — this module only adapts it to the OpenCode hooks.
9
- *
10
- * Hooks (opencode 1.17 contract, verified on host):
11
- * - `experimental.chat.messages.transform` — entries are `{ info, parts }`
12
- * envelopes (role on `info.role`); the pipeline keeps the ORIGINAL array
13
- * reference, so trimming mutates in place; the state is injected as a
14
- * synthetic `{ info, parts }` element. Real O(1) prompt footprint.
15
- * - `experimental.session.compacting` — pushes the state into
16
- * `output.context` so the compaction summary preserves it.
17
- * - `tool.execute.after` — the tool response is `output.output`; a fenced
18
- * ```json `state_patch` block is merged (paper ⊕: null deletes) and saved.
19
- *
20
- * AGENT-SCOPED STATE: the opencode hook inputs carry the session id
21
- * (`input.sessionID`; message envelopes carry `info.sessionID`), so every
22
- * hook scopes the state file to `<cwd>/.skillstate/agents/<session>/` —
23
- * parallel opencode sessions (sub-agents) never last-writer-win over the
24
- * main state. When no session id is available the `'default'` agent is
25
- * used (the plugin trims the history of one session context). Writes go
26
- * through the core cross-process sync lock (`lockStateWrite`) so a
27
- * session state file is never interleaved between processes.
2
+ * `@skillstate/opencode` — the OpenCode **v2** plugin.
3
+ *
4
+ * ── What this replaces ───────────────────────────────────────────────────
5
+ *
6
+ * The v1 integration rewrote the conversation on every model request. It
7
+ * kept the system messages and the last three non-system messages, dropped
8
+ * everything else from `output.messages`, and appended a synthetic
9
+ * `role: "user"` message containing the raw state JSON. The reported
10
+ * failure was that the agent stopped doing the user's task and started
11
+ * emitting state JSON instead.
12
+ *
13
+ * Both halves of that were destructive, and neither was a model quirk:
14
+ *
15
+ * 1. The injected message landed LAST, so for the model it was the current
16
+ * instruction — it displaced the user's actual request.
17
+ * 2. `slice(-3)` deleted the task statement, the tool results and the
18
+ * errors the agent had just been handed. It was reasoning about work it
19
+ * could no longer see.
20
+ *
21
+ * The MCP server made it worse: `spec.get` returned a procedural spec whose
22
+ * default was `INTERCODE_CTF_SPEC`, whose instructions read "You are an
23
+ * autonomous CTF agent ... hidden flag somewhere on its filesystem". A
24
+ * model told to look for a flag looks for a flag. (Fixed: the default is now
25
+ * the neutral `GENERIC_PROCEDURE_SPEC`, and its instructions describe the
26
+ * storage format instead of prescribing a way of working.)
27
+ *
28
+ * ── The v2 design ────────────────────────────────────────────────────────
29
+ *
30
+ * Three rules, each enforced by a test:
31
+ *
32
+ * - **Never mutate `event.messages`.** The plugin contributes one additive
33
+ * fragment to `event.system` and leaves the transcript alone. See
34
+ * `tests/opencode/context-integrity.test.ts`.
35
+ * - **Never inject behavioural instructions.** The system fragment
36
+ * describes what the notes are and when to use them; it contains no
37
+ * "you must", no "always", and no output format. See
38
+ * `system-hint.ts`.
39
+ * - **Inert until used.** A project with no state file gets no system
40
+ * fragment at all and behaves exactly like vanilla OpenCode. No files are
41
+ * created by loading the plugin.
42
+ *
43
+ * ── Native tools AND the MCP server, on purpose ──────────────────────────
44
+ *
45
+ * This package does not replace `@skillstate/mcp`; it sits beside it.
46
+ *
47
+ * - The native tools ({@link registerTools}) are the fast path inside
48
+ * opencode: a typed schema, structured output, no JSON-RPC round-trip and
49
+ * no untyped text result.
50
+ * - The MCP server is the portable path. It is what every other
51
+ * MCP-capable host reads, and the only way to reach this state from a
52
+ * client that is not opencode.
53
+ *
54
+ * Both address the same `<project>/.skillstate/skillstate.json`, so they
55
+ * cannot disagree about what is saved. `skillstate init` registers both.
56
+ *
57
+ * The reason v1 needed the MCP server is gone: an opencode v1 plugin could
58
+ * not contribute first-class tools at all.
59
+ *
60
+ * Load it from `opencode.json(c)`:
61
+ *
62
+ * ```json
63
+ * { "plugins": ["@skillstate/opencode"] }
64
+ * ```
28
65
  */
29
- import * as fs from 'node:fs';
30
- import * as os from 'node:os';
66
+ import { Plugin } from '@opencode/plugin';
31
67
  import * as path from 'node:path';
32
- import { findFencedPatch, lockStateWrite, mergePatch, readStateEnvelope, resolveAgentIdFromSession, resolveHostStateForCwd, saveStateEnvelope, } from '@skillstate/core';
33
- export * from './plugin-types.js';
34
- /**
35
- * Resolve the per-project state file for a session working directory
36
- * (`cwd` of the current opencode session) — the core single source of
37
- * truth (`resolveHostStateForCwd`): `<cwd>/.skillstate/skillstate.json`,
38
- * or the global bucket `<home>/.skillstate/global/skillstate.json` when
39
- * cwd equals home. A non-empty `agentId` scopes the file under
40
- * `<bucket>/agents/<agentId>/skillstate.json`. Pure path arithmetic via
41
- * `path.resolve`, no filesystem access.
42
- */
43
- export { resolveHostStateForCwd as resolveStatePathForCwd };
44
- export { mergePatch };
45
- /** Agent scope used by the plugin when no session id is available. */
46
- export const PLUGIN_DEFAULT_AGENT_ID = 'default';
68
+ import { SessionRegistry, stateScopeFor } from './session-registry.js';
69
+ import { ProjectStateStore } from './state-store.js';
70
+ import { buildStateHint } from './system-hint.js';
71
+ import { registerTools } from './tools.js';
72
+ /** Stable plugin id — scopes plugin storage and identifies it in `/api/plugin`. */
73
+ export const PLUGIN_ID = 'skillstate';
47
74
  /**
48
- * Read the state file. Missing or corrupt files yield `{}` (best-effort).
49
- * The on-disk envelope is `{ version: 1, state }` (migrations-compatible);
50
- * a bare object is tolerated and treated as the state itself. Thin fs
51
- * adapter over the core hook-runtime {@link readStateEnvelope}.
52
- */
53
- export function readSkillState(statePath) {
54
- return readStateEnvelope(statePath, (p) => fs.readFileSync(p, 'utf-8'));
55
- }
56
- /**
57
- * Persist the state file (best-effort: read-only environments are ignored).
58
- * Creates the parent directory when missing (the per-project resolver may
59
- * target a fresh `<cwd>/.skillstate/agents/<id>/`). Writes the
60
- * `{ version: 1, state }` envelope so `migrate()`/runtime resume read the
61
- * same file — via the core hook-runtime {@link saveStateEnvelope} — under
62
- * the cross-process sync lock {@link lockStateWrite} (2-3 parallel agent
63
- * processes never interleave state writes).
64
- */
65
- export function saveSkillState(statePath, state) {
66
- try {
67
- fs.mkdirSync(path.dirname(statePath), { recursive: true });
68
- lockStateWrite(statePath, fs, () => saveStateEnvelope(statePath, state, (p, data) => fs.writeFileSync(p, data)));
69
- }
70
- catch {
71
- // Best-effort: read-only environments or permission issues.
72
- }
73
- }
74
- /**
75
- * Atomic READ-MERGE-WRITE of one `state_patch` (paper ⊕: null deletes):
76
- * the whole critical section runs inside {@link lockStateWrite}, so two
77
- * concurrent writers apply BOTH patches instead of racing between the
78
- * read and the write. Best-effort: lock contention or unwritable state
79
- * files are swallowed — the tool flow never breaks.
75
+ * The plugin definition.
76
+ *
77
+ * `setup` wires three things and returns a cleanup function:
78
+ *
79
+ * - a {@link SessionRegistry}, fed by the server event stream, so a
80
+ * sub-agent session is recognised and given its own state file;
81
+ * - a {@link ProjectStateStore} rooted at the plugin's own project
82
+ * location, so two checkouts served by one OpenCode server never share
83
+ * state;
84
+ * - native tools plus a single additive `context` hook.
85
+ *
86
+ * The event subscription is the only resource the plugin owns, so the
87
+ * returned cleanup aborts it. Hook and tool registrations are disposed by
88
+ * OpenCode when the plugin unloads.
80
89
  */
81
- export function mergeSkillState(statePath, patch) {
82
- try {
83
- fs.mkdirSync(path.dirname(statePath), { recursive: true });
84
- let merged = {};
85
- lockStateWrite(statePath, fs, () => {
86
- merged = mergePatch(readSkillState(statePath), patch);
87
- saveStateEnvelope(statePath, merged, (p, data) => fs.writeFileSync(p, data));
90
+ export const SkillStatePlugin = Plugin.define({
91
+ id: PLUGIN_ID,
92
+ async setup(ctx) {
93
+ const sessions = new SessionRegistry();
94
+ const scopeFor = (sessionID) => stateScopeFor(sessions, sessionID);
95
+ // `ctx.location.project.canonical` is the canonical checkout, stable
96
+ // across worktrees and symlinks. The v1 plugin used `process.cwd()`,
97
+ // which in v2 is the server's cwd, not the session's project.
98
+ const store = new ProjectStateStore({
99
+ directory: ctx.location.project.canonical,
88
100
  });
89
- return merged;
90
- }
91
- catch {
92
- return readSkillState(statePath);
93
- }
94
- }
95
- /**
96
- * Extract the `state_patch` object from an LLM response's fenced ```json
97
- * block; `null` when there is no block, it is malformed, or it carries no
98
- * object-shaped `state_patch`. Thin adapter over the core hook-runtime
99
- * {@link findFencedPatch} (the invalid/truncated outcomes collapse to
100
- * `null`, preserving the legacy boolean contract).
101
- */
102
- export function extractPatch(response) {
103
- const result = findFencedPatch(response);
104
- return 'patch' in result ? result.patch : null;
105
- }
106
- /**
107
- * Agent id for an opencode hook call: the 8-char session prefix from
108
- * `input.sessionID` when the hook carries it, else the first non-synthetic
109
- * message's `info.sessionID` (the transform hook input is empty — the
110
- * session lives on the message envelopes), else `'default'` (the plugin
111
- * trims the history of one session context).
112
- */
113
- export function pluginAgentId(input, messages) {
114
- const direct = resolveAgentIdFromSession(input?.sessionID);
115
- if (direct.length > 0)
116
- return direct;
117
- const fromMessages = (messages ?? []).find((m) => typeof m.info?.sessionID === 'string' &&
118
- m.info.sessionID.length > 0 &&
119
- m.info.sessionID !== 'skillstate');
120
- const indirect = resolveAgentIdFromSession(fromMessages?.info.sessionID);
121
- return indirect.length > 0 ? indirect : PLUGIN_DEFAULT_AGENT_ID;
122
- }
123
- /** Synthetic message ids for the injected state carrier. */
124
- const STATE_MESSAGE_ID = 'skillstate-state-inject';
125
- /**
126
- * Build the OpenCode plugin function with the same behavior for every host
127
- * entry point (thin generated loaders, direct imports).
128
- *
129
- * State resolution is ALWAYS per-project: the state file path is computed
130
- * from the session cwd on EVERY hook call via
131
- * `resolveStatePathForCwd(process.cwd(), os.homedir(), agentId)` — each
132
- * project gets its own `<cwd>/.skillstate/`, each session (sub-agent) its
133
- * isolated `agents/<session>/` copy, and a session launched from `$HOME`
134
- * uses the global bucket.
135
- */
136
- export function createSkillStatePlugin(options = {}) {
137
- const resolvePath = (agentId) => resolveHostStateForCwd(process.cwd(), os.homedir(), agentId);
138
- const maxHistory = options.maxHistoryMessages ?? 3;
139
- return async () => {
140
- return {
141
- // ── O(1) history trimming ──────────────────────────────────────────
142
- // Filters messages BEFORE each LLM call: keeps all system messages
143
- // plus the last `maxHistory` non-system messages, then injects a
144
- // synthetic state element. Old messages are DROPPED from the prompt,
145
- // not just hidden.
146
- 'experimental.chat.messages.transform': async (input, output) => {
147
- const agentId = pluginAgentId(input, output.messages);
148
- const state = readSkillState(resolvePath(agentId));
149
- const messages = output.messages;
150
- const systemMessages = messages.filter((m) => m.info.role === 'system');
151
- const trimmed = messages
152
- .filter((m) => m.info.role !== 'system')
153
- .slice(-maxHistory);
154
- // Synthetic state carrier — a `{ info, parts }` envelope whose text
155
- // part carries the current state JSON.
156
- const stateMessage = {
157
- info: {
158
- id: STATE_MESSAGE_ID,
159
- sessionID: 'skillstate',
160
- role: 'user',
161
- time: { created: 0 },
162
- agent: 'skillstate',
163
- model: { providerID: 'skillstate', modelID: 'skillstate' },
164
- },
165
- parts: [
166
- {
167
- id: `${STATE_MESSAGE_ID}-text`,
168
- sessionID: 'skillstate',
169
- messageID: STATE_MESSAGE_ID,
170
- type: 'text',
171
- synthetic: true,
172
- text: `Current skill state (JSON): ${JSON.stringify(state)}`,
173
- },
174
- ],
175
- };
176
- // The pipeline holds the original array reference — mutate in place
177
- // (reassigning `output.messages` would not reach the LLM call).
178
- const kept = [...systemMessages, ...trimmed, stateMessage];
179
- messages.length = 0;
180
- messages.push(...kept);
181
- },
182
- // ── Compaction context injection ───────────────────────────────────
183
- // Before compaction, inject the current state into the context so the
184
- // compaction summary preserves state even after history is compressed.
185
- 'experimental.session.compacting': async (input, output) => {
186
- const agentId = pluginAgentId(input);
187
- const state = readSkillState(resolvePath(agentId));
188
- if (!Array.isArray(output.context)) {
189
- output.context = [];
190
- }
191
- output.context.push(`Skillstate: ${JSON.stringify(state)}`);
192
- },
193
- // ── State persistence from LLM responses ───────────────────────────
194
- // After tool execution, extract state_patch from the tool response
195
- // (output.output), and atomically merge it into the session-scoped
196
- // state (read + merge + write all inside the cross-process lock).
197
- 'tool.execute.after': async (input, output) => {
198
- const response = output.output ?? '';
199
- if (typeof response !== 'string')
200
- return;
201
- const patch = extractPatch(response);
202
- if (patch) {
203
- const agentId = pluginAgentId(input);
204
- mergeSkillState(resolvePath(agentId), patch);
101
+ await ctx.tool.transform((editor) => {
102
+ registerTools(editor, { store, sessions, scopeFor });
103
+ });
104
+ // ── Session tree ────────────────────────────────────────────────────
105
+ // Sub-agent sessions are created by OpenCode itself, so the parent edge
106
+ // arrives on the event stream. Until one is seen a session is treated as
107
+ // a root session, which is the correct default for single-session use.
108
+ const controller = new AbortController();
109
+ void (async () => {
110
+ try {
111
+ for await (const event of ctx.event.subscribe({ signal: controller.signal })) {
112
+ sessions.ingestEvent(event);
205
113
  }
206
- },
114
+ }
115
+ catch {
116
+ // The stream ends when the plugin unloads or the server goes away.
117
+ // Session scoping degrades to "everyone shares the project file",
118
+ // which is safe; it must never surface as an unhandled rejection.
119
+ }
120
+ })();
121
+ // ── System fragment ─────────────────────────────────────────────────
122
+ // Registered on the agent loop only. `compaction`, `generate` and
123
+ // `title` are separate hooks in v2 and are deliberately left alone:
124
+ // after a compaction the next agent-loop request re-adds the fragment,
125
+ // so state survives without this plugin ever touching the transcript or
126
+ // the summariser's input.
127
+ await ctx.session.hook('context', (event) => {
128
+ const scope = scopeFor(event.sessionID);
129
+ if (!store.exists(scope))
130
+ return;
131
+ const state = store.read(scope);
132
+ const hint = buildStateHint({
133
+ state,
134
+ statePath: path.relative(store.projectDirectory, store.pathFor(scope)),
135
+ scope,
136
+ });
137
+ if (hint.length === 0)
138
+ return;
139
+ event.system.push({ type: 'text', text: hint });
140
+ });
141
+ return () => {
142
+ controller.abort();
207
143
  };
208
- };
209
- }
144
+ },
145
+ });
146
+ export default SkillStatePlugin;
210
147
  //# sourceMappingURL=plugin.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"plugin.js","sourceRoot":"","sources":["../src/plugin.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AACH,OAAO,KAAK,EAAE,MAAM,SAAS,CAAC;AAC9B,OAAO,KAAK,EAAE,MAAM,SAAS,CAAC;AAC9B,OAAO,KAAK,IAAI,MAAM,WAAW,CAAC;AAClC,OAAO,EACL,eAAe,EACf,cAAc,EACd,UAAU,EACV,iBAAiB,EACjB,yBAAyB,EACzB,sBAAsB,EACtB,iBAAiB,GAClB,MAAM,kBAAkB,CAAC;AAO1B,cAAc,mBAAmB,CAAC;AAElC;;;;;;;;GAQG;AACH,OAAO,EAAE,sBAAsB,IAAI,sBAAsB,EAAE,CAAC;AAE5D,OAAO,EAAE,UAAU,EAAE,CAAC;AAEtB,sEAAsE;AACtE,MAAM,CAAC,MAAM,uBAAuB,GAAG,SAAS,CAAC;AAQjD;;;;;GAKG;AACH,MAAM,UAAU,cAAc,CAAC,SAAiB;IAC9C,OAAO,iBAAiB,CAAC,SAAS,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,EAAE,CAAC,YAAY,CAAC,CAAC,EAAE,OAAO,CAAC,CAA4B,CAAC;AACrG,CAAC;AAED;;;;;;;;GAQG;AACH,MAAM,UAAU,cAAc,CAAC,SAAiB,EAAE,KAA8B;IAC9E,IAAI,CAAC;QACH,EAAE,CAAC,SAAS,CAAC,IAAI,CAAC,OAAO,CAAC,SAAS,CAAC,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;QAC3D,cAAc,CACZ,SAAS,EACT,EAAE,EACF,GAAG,EAAE,CAAC,iBAAiB,CAAC,SAAS,EAAE,KAAK,EAAE,CAAC,CAAC,EAAE,IAAI,EAAE,EAAE,CAAC,EAAE,CAAC,aAAa,CAAC,CAAC,EAAE,IAAI,CAAC,CAAC,CAClF,CAAC;IACJ,CAAC;IAAC,MAAM,CAAC;QACP,4DAA4D;IAC9D,CAAC;AACH,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,eAAe,CAC7B,SAAiB,EACjB,KAA8B;IAE9B,IAAI,CAAC;QACH,EAAE,CAAC,SAAS,CAAC,IAAI,CAAC,OAAO,CAAC,SAAS,CAAC,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;QAC3D,IAAI,MAAM,GAA4B,EAAE,CAAC;QACzC,cAAc,CAAC,SAAS,EAAE,EAAE,EAAE,GAAG,EAAE;YACjC,MAAM,GAAG,UAAU,CAAC,cAAc,CAAC,SAAS,CAAC,EAAE,KAAK,CAAC,CAAC;YACtD,iBAAiB,CAAC,SAAS,EAAE,MAAM,EAAE,CAAC,CAAC,EAAE,IAAI,EAAE,EAAE,CAAC,EAAE,CAAC,aAAa,CAAC,CAAC,EAAE,IAAI,CAAC,CAAC,CAAC;QAC/E,CAAC,CAAC,CAAC;QACH,OAAO,MAAM,CAAC;IAChB,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,cAAc,CAAC,SAAS,CAAC,CAAC;IACnC,CAAC;AACH,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,YAAY,CAAC,QAAgB;IAC3C,MAAM,MAAM,GAAG,eAAe,CAAC,QAAQ,CAAC,CAAC;IACzC,OAAO,OAAO,IAAI,MAAM,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,IAAI,CAAC;AACjD,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,aAAa,CAC3B,KAA8B,EAC9B,QAA4B;IAE5B,MAAM,MAAM,GAAG,yBAAyB,CAAC,KAAK,EAAE,SAAS,CAAC,CAAC;IAC3D,IAAI,MAAM,CAAC,MAAM,GAAG,CAAC;QAAE,OAAO,MAAM,CAAC;IACrC,MAAM,YAAY,GAAG,CAAC,QAAQ,IAAI,EAAE,CAAC,CAAC,IAAI,CACxC,CAAC,CAAC,EAAE,EAAE,CACJ,OAAO,CAAC,CAAC,IAAI,EAAE,SAAS,KAAK,QAAQ;QACrC,CAAC,CAAC,IAAI,CAAC,SAAS,CAAC,MAAM,GAAG,CAAC;QAC3B,CAAC,CAAC,IAAI,CAAC,SAAS,KAAK,YAAY,CACpC,CAAC;IACF,MAAM,QAAQ,GAAG,yBAAyB,CAAC,YAAY,EAAE,IAAI,CAAC,SAAS,CAAC,CAAC;IACzE,OAAO,QAAQ,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,uBAAuB,CAAC;AAClE,CAAC;AAED,4DAA4D;AAC5D,MAAM,gBAAgB,GAAG,yBAAyB,CAAC;AAEnD;;;;;;;;;;GAUG;AACH,MAAM,UAAU,sBAAsB,CAAC,OAAO,GAA4B,EAAE;IAC1E,MAAM,WAAW,GAAG,CAAC,OAAe,EAAU,EAAE,CAC9C,sBAAsB,CAAC,OAAO,CAAC,GAAG,EAAE,EAAE,EAAE,CAAC,OAAO,EAAE,EAAE,OAAO,CAAC,CAAC;IAC/D,MAAM,UAAU,GAAG,OAAO,CAAC,kBAAkB,IAAI,CAAC,CAAC;IAEnD,OAAO,KAAK,IAAI,EAAE;QAChB,OAAO;YACL,sEAAsE;YACtE,mEAAmE;YACnE,iEAAiE;YACjE,qEAAqE;YACrE,mBAAmB;YACnB,sCAAsC,EAAE,KAAK,EAC3C,KAAK,EACL,MAAM,EACS,EAAE;gBACjB,MAAM,OAAO,GAAG,aAAa,CAAC,KAAgC,EAAE,MAAM,CAAC,QAAQ,CAAC,CAAC;gBACjF,MAAM,KAAK,GAAG,cAAc,CAAC,WAAW,CAAC,OAAO,CAAC,CAAC,CAAC;gBACnD,MAAM,QAAQ,GAAG,MAAM,CAAC,QAAQ,CAAC;gBACjC,MAAM,cAAc,GAAG,QAAQ,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,KAAK,QAAQ,CAAC,CAAC;gBACxE,MAAM,OAAO,GAAG,QAAQ;qBACrB,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,KAAK,QAAQ,CAAC;qBACvC,KAAK,CAAC,CAAC,UAAU,CAAC,CAAC;gBAEtB,oEAAoE;gBACpE,uCAAuC;gBACvC,MAAM,YAAY,GAAoB;oBACpC,IAAI,EAAE;wBACJ,EAAE,EAAE,gBAAgB;wBACpB,SAAS,EAAE,YAAY;wBACvB,IAAI,EAAE,MAAM;wBACZ,IAAI,EAAE,EAAE,OAAO,EAAE,CAAC,EAAE;wBACpB,KAAK,EAAE,YAAY;wBACnB,KAAK,EAAE,EAAE,UAAU,EAAE,YAAY,EAAE,OAAO,EAAE,YAAY,EAAE;qBAC3D;oBACD,KAAK,EAAE;wBACL;4BACE,EAAE,EAAE,GAAG,gBAAgB,OAAO;4BAC9B,SAAS,EAAE,YAAY;4BACvB,SAAS,EAAE,gBAAgB;4BAC3B,IAAI,EAAE,MAAM;4BACZ,SAAS,EAAE,IAAI;4BACf,IAAI,EAAE,+BAA+B,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,EAAE;yBAC7D;qBACF;iBACF,CAAC;gBAEF,oEAAoE;gBACpE,gEAAgE;gBAChE,MAAM,IAAI,GAAG,CAAC,GAAG,cAAc,EAAE,GAAG,OAAO,EAAE,YAAY,CAAC,CAAC;gBAC3D,QAAQ,CAAC,MAAM,GAAG,CAAC,CAAC;gBACpB,QAAQ,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC,CAAC;YACzB,CAAC;YAED,sEAAsE;YACtE,sEAAsE;YACtE,uEAAuE;YACvE,iCAAiC,EAAE,KAAK,EAAE,KAAK,EAAE,MAAM,EAAiB,EAAE;gBACxE,MAAM,OAAO,GAAG,aAAa,CAAC,KAAK,CAAC,CAAC;gBACrC,MAAM,KAAK,GAAG,cAAc,CAAC,WAAW,CAAC,OAAO,CAAC,CAAC,CAAC;gBACnD,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,MAAM,CAAC,OAAO,CAAC,EAAE,CAAC;oBACnC,MAAM,CAAC,OAAO,GAAG,EAAE,CAAC;gBACtB,CAAC;gBACD,MAAM,CAAC,OAAO,CAAC,IAAI,CAAC,eAAe,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC;YAC9D,CAAC;YAED,sEAAsE;YACtE,mEAAmE;YACnE,mEAAmE;YACnE,kEAAkE;YAClE,oBAAoB,EAAE,KAAK,EAAE,KAAK,EAAE,MAAM,EAAiB,EAAE;gBAC3D,MAAM,QAAQ,GAAG,MAAM,CAAC,MAAM,IAAI,EAAE,CAAC;gBACrC,IAAI,OAAO,QAAQ,KAAK,QAAQ;oBAAE,OAAO;gBACzC,MAAM,KAAK,GAAG,YAAY,CAAC,QAAQ,CAAC,CAAC;gBACrC,IAAI,KAAK,EAAE,CAAC;oBACV,MAAM,OAAO,GAAG,aAAa,CAAC,KAAK,CAAC,CAAC;oBACrC,eAAe,CAAC,WAAW,CAAC,OAAO,CAAC,EAAE,KAAK,CAAC,CAAC;gBAC/C,CAAC;YACH,CAAC;SACwB,CAAC;IAC9B,CAAC,CAAC;AACJ,CAAC"}
1
+ {"version":3,"file":"plugin.js","sourceRoot":"","sources":["../src/plugin.ts"],"names":[],"mappings":"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"}