@kooka/agent-sdk 0.1.0 → 0.1.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.
Files changed (35) hide show
  1. package/README.md +143 -2
  2. package/dist/agent/agent.d.ts +11 -0
  3. package/dist/agent/agent.d.ts.map +1 -1
  4. package/dist/agent/agent.js +132 -7
  5. package/dist/agent/agent.js.map +1 -1
  6. package/dist/index.d.ts +2 -0
  7. package/dist/index.d.ts.map +1 -1
  8. package/dist/index.js +14 -1
  9. package/dist/index.js.map +1 -1
  10. package/dist/persistence/index.d.ts +5 -0
  11. package/dist/persistence/index.d.ts.map +1 -0
  12. package/dist/persistence/index.js +3 -0
  13. package/dist/persistence/index.js.map +1 -0
  14. package/dist/persistence/sessionSnapshot.d.ts +37 -0
  15. package/dist/persistence/sessionSnapshot.d.ts.map +1 -0
  16. package/dist/persistence/sessionSnapshot.js +53 -0
  17. package/dist/persistence/sessionSnapshot.js.map +1 -0
  18. package/dist/persistence/sqliteSessionStore.d.ts +38 -0
  19. package/dist/persistence/sqliteSessionStore.d.ts.map +1 -0
  20. package/dist/persistence/sqliteSessionStore.js +75 -0
  21. package/dist/persistence/sqliteSessionStore.js.map +1 -0
  22. package/dist/tools/agentBrowser.d.ts +131 -0
  23. package/dist/tools/agentBrowser.d.ts.map +1 -0
  24. package/dist/tools/agentBrowser.js +747 -0
  25. package/dist/tools/agentBrowser.js.map +1 -0
  26. package/dist/tools/builtin/index.d.ts +3 -0
  27. package/dist/tools/builtin/index.d.ts.map +1 -1
  28. package/dist/tools/builtin/index.js.map +1 -1
  29. package/dist/tools/builtin/skill.d.ts.map +1 -1
  30. package/dist/tools/builtin/skill.js +5 -7
  31. package/dist/tools/builtin/skill.js.map +1 -1
  32. package/dist/types.d.ts +12 -0
  33. package/dist/types.d.ts.map +1 -1
  34. package/package.json +19 -12
  35. package/LICENSE +0 -201
package/README.md CHANGED
@@ -24,6 +24,8 @@ pnpm install
24
24
  pnpm --filter @kooka/agent-sdk build
25
25
  ```
26
26
 
27
+ Tip: in a clean monorepo checkout, this auto-builds missing `@kooka/core` outputs (types + runtime entrypoints).
28
+
27
29
  ### As a dependency (local path)
28
30
 
29
31
  In another project’s `package.json`:
@@ -99,7 +101,31 @@ try {
99
101
  }
100
102
  ```
101
103
 
102
- CommonJS:
104
+ ## Custom Tools
105
+
106
+ `createLingyunAgent(...)` returns a `ToolRegistry` so hosts can register their own tools:
107
+
108
+ ```ts
109
+ import { createLingyunAgent, type ToolDefinition } from '@kooka/agent-sdk';
110
+
111
+ const { agent, registry } = createLingyunAgent({ /* ... */ });
112
+
113
+ const timeTool: ToolDefinition = {
114
+ id: 'time.now',
115
+ name: 'time.now',
116
+ description: 'Get the current time as an ISO string.',
117
+ parameters: { type: 'object', properties: {} },
118
+ execution: { type: 'function', handler: 'time.now' },
119
+ metadata: { readOnly: true },
120
+ };
121
+
122
+ registry.registerTool(timeTool, async () => ({
123
+ success: true,
124
+ data: { now: new Date().toISOString() },
125
+ }));
126
+ ```
127
+
128
+ ## CommonJS
103
129
 
104
130
  ```js
105
131
  const { createLingyunAgent, LingyunSession } = await import('@kooka/agent-sdk')
@@ -136,6 +162,7 @@ Useful event types:
136
162
 
137
163
  - `assistant_token`: user-facing assistant text (with `<think>` and tool-call markers removed)
138
164
  - `thought_token`: model “thinking” tokens (only if the provider emits them)
165
+ - `notice`: user-facing notices from the runtime (e.g. unknown `$skill-name`)
139
166
  - `tool_call` / `tool_result` / `tool_blocked`: tool lifecycle
140
167
  - `compaction_start` / `compaction_end`: context overflow mitigation
141
168
 
@@ -153,6 +180,38 @@ You can disable built-ins:
153
180
  createLingyunAgent({ llm: { /*...*/ }, tools: { builtin: false } })
154
181
  ```
155
182
 
183
+ ### Skills (`$skill-name`)
184
+
185
+ The SDK supports Codex-style `$skill-name` mentions.
186
+ If a user message includes `$<skill-name>`, LingYun:
187
+
188
+ 1. Looks up the skill by `name:` in discovered `SKILL.md` files
189
+ 2. Injects the skill body as a synthetic `<skill>...</skill>` user message before calling the model
190
+
191
+ Unknown skills are ignored and emitted as `notice` events (`callbacks.onNotice`).
192
+
193
+ Configure discovery/injection via `tools.builtinOptions.skills`:
194
+
195
+ ```ts
196
+ createLingyunAgent({
197
+ llm: { provider: 'openaiCompatible', baseURL: 'http://localhost:8080/v1', model: 'your-model-id' },
198
+ workspaceRoot: process.cwd(),
199
+ tools: {
200
+ builtinOptions: {
201
+ skills: {
202
+ enabled: true,
203
+ paths: ['.lingyun/skills', '~/.codex/skills'],
204
+ maxPromptSkills: 50,
205
+ maxInjectSkills: 5,
206
+ maxInjectChars: 20_000,
207
+ },
208
+ },
209
+ },
210
+ });
211
+ ```
212
+
213
+ Note: the “Available skills” list included in the system prompt is built once per `LingyunAgent` instance. Create a new agent to refresh it.
214
+
156
215
  ### Approvals
157
216
 
158
217
  Tools can require approval via `ToolDefinition.metadata.requiresApproval`.
@@ -191,6 +250,43 @@ registry.registerTool(
191
250
 
192
251
  Return formatting hints via `ToolResult.metadata.outputText` / `title` to control what the agent sees.
193
252
 
253
+ ### Browser automation (agent-browser)
254
+
255
+ If you install `agent-browser` and Chromium, you can register an **interactive browser toolset** (sessions + snapshot + actions):
256
+
257
+ ```bash
258
+ npm i -g agent-browser
259
+ agent-browser install
260
+ ```
261
+
262
+ Then in your host:
263
+
264
+ ```ts
265
+ import { createLingyunAgent, registerAgentBrowserTools } from '@kooka/agent-sdk';
266
+
267
+ const { registry } = createLingyunAgent({
268
+ llm: { provider: 'openaiCompatible', baseURL: 'http://localhost:8080/v1', model: 'your-model-id' },
269
+ workspaceRoot: process.cwd(),
270
+ });
271
+
272
+ registerAgentBrowserTools(registry, {
273
+ artifactsDir: '.kooka/agent-browser',
274
+ timeoutMs: 30_000,
275
+ });
276
+ ```
277
+
278
+ Tools:
279
+ - `browser.startSession` / `browser.closeSession`
280
+ - `browser.snapshot` (read-only; returns accessibility tree with refs like `@e2`)
281
+ - `browser.run` (requires approval; runs click/fill/type/wait/get/screenshot/pdf/trace actions)
282
+
283
+ Security defaults:
284
+ - HTTPS-only and blocks private hosts / IPs by default
285
+ - No cookies/storage/state/headers APIs are exposed by this toolset (no auth-state support)
286
+ - Screenshot/PDF/trace artifacts are written under `artifactsDir` (relative to `workspaceRoot` when set)
287
+
288
+ If `agent-browser` is not on PATH, set `AGENT_BROWSER_BIN` or pass `agentBrowserBin` to `registerAgentBrowserTools`.
289
+
194
290
  ## Inspiration / Compatibility
195
291
 
196
292
  - **OpenCode SDK**: OpenCode’s JavaScript SDK primarily wraps an HTTP server (client + server helpers). LingYun SDK starts with an **in‑process** agent runtime that can later be wrapped by an HTTP server if needed.
@@ -215,7 +311,52 @@ A session holds:
215
311
  - message history (OpenCode-aligned “assistant message + parts”)
216
312
  - any pending plan text (optional)
217
313
 
218
- Sessions are serializable; persistence is the host application’s responsibility.
314
+ Sessions are serializable; persistence is the host application’s responsibility. The SDK does not write to disk.
315
+
316
+ You can snapshot + restore:
317
+
318
+ ```ts
319
+ import { LingyunSession, snapshotSession, restoreSession } from '@kooka/agent-sdk';
320
+
321
+ const session = new LingyunSession({ sessionId: 's1' });
322
+
323
+ const snapshot = snapshotSession(session, {
324
+ sessionId: 's1',
325
+ // includeFileHandles: false, // omit fileId/path hints if you don't want to persist them
326
+ });
327
+
328
+ // Persist `snapshot` however you want (JSON files, sqlite, postgres, ...).
329
+
330
+ const restored = restoreSession(snapshot);
331
+ ```
332
+
333
+ If you want SQLite, the SDK ships a `SqliteSessionStore` that works with any driver you provide:
334
+
335
+ ```ts
336
+ import Database from 'better-sqlite3';
337
+ import { SqliteSessionStore, snapshotSession, restoreSession, type SqliteDriver } from '@kooka/agent-sdk';
338
+
339
+ const db = new Database('lingyun.db');
340
+
341
+ const driver: SqliteDriver = {
342
+ execute: (sql, params = []) => void db.prepare(sql).run(...params),
343
+ queryOne: (sql, params = []) => db.prepare(sql).get(...params),
344
+ queryAll: (sql, params = []) => db.prepare(sql).all(...params),
345
+ };
346
+
347
+ const store = new SqliteSessionStore(driver);
348
+
349
+ const sessionId = 's1';
350
+ const session = new LingyunSession({ sessionId });
351
+
352
+ await store.save(sessionId, snapshotSession(session, { sessionId }));
353
+ const loaded = await store.load(sessionId);
354
+ const loadedSession = loaded ? restoreSession(loaded) : new LingyunSession({ sessionId });
355
+ ```
356
+
357
+ Notes:
358
+ - The SDK does not bundle a SQLite client library; you bring your own (e.g. `better-sqlite3`, `sqlite3`).
359
+ - Session snapshots contain conversation text and may include file paths; treat persisted data as sensitive.
219
360
 
220
361
  ### Run + Streaming
221
362
 
@@ -17,6 +17,13 @@ export type LingyunAgentRuntimeOptions = {
17
17
  plugins?: PluginManager;
18
18
  workspaceRoot?: string;
19
19
  allowExternalPaths?: boolean;
20
+ skills?: {
21
+ enabled?: boolean;
22
+ paths?: string[];
23
+ maxPromptSkills?: number;
24
+ maxInjectSkills?: number;
25
+ maxInjectChars?: number;
26
+ };
20
27
  modelLimits?: Record<string, ModelLimit>;
21
28
  compaction?: Partial<CompactionConfig>;
22
29
  };
@@ -27,6 +34,8 @@ export declare class LingyunAgent {
27
34
  private readonly plugins;
28
35
  private readonly workspaceRoot?;
29
36
  private allowExternalPaths;
37
+ private readonly skillsConfig;
38
+ private skillsPromptTextPromise?;
30
39
  private readonly modelLimits?;
31
40
  private readonly compactionConfig;
32
41
  private registeredPluginTools;
@@ -54,9 +63,11 @@ export declare class LingyunAgent {
54
63
  private decorateGlobResultWithFileHandles;
55
64
  private createAISDKTools;
56
65
  private filterTools;
66
+ private getSkillsPromptText;
57
67
  private composeSystemPrompt;
58
68
  private compactSessionInternal;
59
69
  private runOnce;
70
+ private injectSkillsForUserText;
60
71
  run(params: {
61
72
  session: LingyunSession;
62
73
  input: string;
@@ -1 +1 @@
1
- {"version":3,"file":"agent.d.ts","sourceRoot":"","sources":["../../src/agent/agent.ts"],"names":[],"mappings":"AAYA,OAAO,KAAK,EAAwC,cAAc,EAAE,WAAW,EAAE,WAAW,EAAgB,UAAU,EAAE,MAAM,aAAa,CAAC;AAE5I,OAAO,EAsBL,KAAK,mBAAmB,EACxB,KAAK,gBAAgB,EACrB,KAAK,UAAU,EAGhB,MAAM,aAAa,CAAC;AACrB,OAAO,EAAE,aAAa,EAAE,MAAM,6BAA6B,CAAC;AAK5D,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,sBAAsB,CAAC;AA8GzD,qBAAa,cAAc;IACzB,OAAO,EAAE,mBAAmB,EAAE,CAAM;IACpC,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,WAAW,CAAC,EAAE;QACZ,MAAM,EAAE,MAAM,CAAC;QACf,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;KAC9B,CAAC;gBAEU,IAAI,CAAC,EAAE,OAAO,CAAC,IAAI,CAAC,cAAc,EAAE,SAAS,GAAG,aAAa,GAAG,WAAW,GAAG,aAAa,CAAC,CAAC;IAOzG,UAAU,IAAI,mBAAmB,EAAE;CAGpC;AAED,MAAM,MAAM,0BAA0B,GAAG;IACvC,OAAO,CAAC,EAAE,aAAa,CAAC;IACxB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,kBAAkB,CAAC,EAAE,OAAO,CAAC;IAC7B,WAAW,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,UAAU,CAAC,CAAC;IACzC,UAAU,CAAC,EAAE,OAAO,CAAC,gBAAgB,CAAC,CAAC;CACxC,CAAC;AAEF,qBAAa,YAAY;IASrB,OAAO,CAAC,QAAQ,CAAC,GAAG;IACpB,OAAO,CAAC,MAAM;IACd,OAAO,CAAC,QAAQ,CAAC,QAAQ;IAV3B,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAgB;IACxC,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAC,CAAS;IACxC,OAAO,CAAC,kBAAkB,CAAU;IACpC,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAC,CAA6B;IAC1D,OAAO,CAAC,QAAQ,CAAC,gBAAgB,CAAmB;IACpD,OAAO,CAAC,qBAAqB,CAAqB;gBAG/B,GAAG,EAAE,WAAW,EACzB,MAAM,EAAE,WAAW,EACV,QAAQ,EAAE,YAAY,EACvC,OAAO,CAAC,EAAE,0BAA0B;IAsBtC,YAAY,CAAC,MAAM,EAAE,OAAO,CAAC,WAAW,CAAC,GAAG,IAAI;IAIhD,qBAAqB,CAAC,KAAK,EAAE,OAAO,GAAG,IAAI;IAI3C,OAAO,CAAC,OAAO;IAIf,OAAO,CAAC,aAAa;IAIrB,OAAO,CAAC,oBAAoB;IAe5B,OAAO,CAAC,iBAAiB;IAQzB,OAAO,CAAC,uBAAuB;IAiB/B,OAAO,CAAC,qBAAqB;IAuB7B,OAAO,CAAC,uBAAuB;IA0B/B,OAAO,CAAC,wBAAwB;IAMhC,OAAO,CAAC,kBAAkB;YAMZ,2BAA2B;YAiD3B,eAAe;IAgB7B,OAAO,CAAC,iBAAiB;YAUX,gBAAgB;YA+BhB,yBAAyB;IA2BvC,OAAO,CAAC,uBAAuB;IAmB/B,OAAO,CAAC,iBAAiB;IAgBzB,OAAO,CAAC,iBAAiB;IASzB,OAAO,CAAC,qBAAqB;IAmB7B,OAAO,CAAC,iCAAiC;IAgDzC,OAAO,CAAC,gBAAgB;IA8PxB,OAAO,CAAC,WAAW;IAiBnB,OAAO,CAAC,mBAAmB;YAWb,sBAAsB;YAkHtB,OAAO;IAoQrB,GAAG,CAAC,MAAM,EAAE;QAAE,OAAO,EAAE,cAAc,CAAC;QAAC,KAAK,EAAE,MAAM,CAAC;QAAC,SAAS,CAAC,EAAE,cAAc,CAAC;QAAC,MAAM,CAAC,EAAE,WAAW,CAAA;KAAE,GAAG,UAAU;CAqEtH"}
1
+ {"version":3,"file":"agent.d.ts","sourceRoot":"","sources":["../../src/agent/agent.ts"],"names":[],"mappings":"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"}
@@ -1,12 +1,14 @@
1
1
  import * as path from 'path';
2
2
  import { convertToModelMessages, extractReasoningMiddleware, jsonSchema, streamText, tool as aiTool, wrapLanguageModel, } from 'ai';
3
3
  import { combineAbortSignals } from '../abort.js';
4
- import { COMPACTION_AUTO_CONTINUE_TEXT, COMPACTION_MARKER_TEXT, COMPACTION_PROMPT_TEXT, COMPACTION_SYSTEM_PROMPT, createAssistantHistoryMessage, createHistoryForModel, createUserHistoryMessage, evaluatePermission, evaluateShellCommand, extractUsageTokens, finalizeStreamingParts, findExternalPathReferencesInShellCommand, getEffectiveHistory, getMessageText, getReservedOutputTokens, isOverflow as isContextOverflow, isPathInsideWorkspace, markPrunableToolOutputs, setDynamicToolError, setDynamicToolOutput, upsertDynamicToolCall, } from '@kooka/core';
4
+ import { COMPACTION_AUTO_CONTINUE_TEXT, COMPACTION_MARKER_TEXT, COMPACTION_PROMPT_TEXT, COMPACTION_SYSTEM_PROMPT, createAssistantHistoryMessage, createHistoryForCompactionPrompt, createHistoryForModel, createUserHistoryMessage, evaluatePermission, evaluateShellCommand, extractSkillMentions, extractUsageTokens, finalizeStreamingParts, findExternalPathReferencesInShellCommand, getEffectiveHistory, getMessageText, getReservedOutputTokens, isOverflow as isContextOverflow, isPathInsideWorkspace, markPreviousAssistantToolOutputs, redactFsPathForPrompt, renderSkillsSectionForPrompt, selectSkillsForText, setDynamicToolError, setDynamicToolOutput, upsertDynamicToolCall, } from '@kooka/core';
5
5
  import { PluginManager } from '../plugins/pluginManager.js';
6
6
  import { insertModeReminders } from './reminders.js';
7
7
  import { DEFAULT_SYSTEM_PROMPT } from './prompts.js';
8
8
  import { EDIT_TOOL_IDS, MAX_TOOL_RESULT_LENGTH, THINK_BLOCK_REGEX, TOOL_BLOCK_REGEX } from './constants.js';
9
9
  import { delay as getRetryDelayMs, retryable as getRetryableLlmError, sleep as retrySleep } from './retry.js';
10
+ import { DEFAULT_SKILL_PATHS } from '../tools/builtin/index.js';
11
+ import { getSkillIndex, loadSkillFile } from '../skills.js';
10
12
  class AsyncQueue {
11
13
  state = {
12
14
  values: [],
@@ -127,6 +129,8 @@ export class LingyunAgent {
127
129
  plugins;
128
130
  workspaceRoot;
129
131
  allowExternalPaths;
132
+ skillsConfig;
133
+ skillsPromptTextPromise;
130
134
  modelLimits;
131
135
  compactionConfig;
132
136
  registeredPluginTools = new Set();
@@ -137,12 +141,31 @@ export class LingyunAgent {
137
141
  this.plugins = runtime?.plugins ?? new PluginManager({ workspaceRoot: runtime?.workspaceRoot });
138
142
  this.workspaceRoot = runtime?.workspaceRoot ? path.resolve(runtime.workspaceRoot) : undefined;
139
143
  this.allowExternalPaths = !!runtime?.allowExternalPaths;
144
+ const skills = runtime?.skills ?? {};
145
+ const paths = Array.isArray(skills.paths) && skills.paths.length > 0 ? skills.paths : DEFAULT_SKILL_PATHS;
146
+ const maxPromptSkills = Number.isFinite(skills.maxPromptSkills) && skills.maxPromptSkills >= 0
147
+ ? Math.floor(skills.maxPromptSkills)
148
+ : 50;
149
+ const maxInjectSkills = Number.isFinite(skills.maxInjectSkills) && skills.maxInjectSkills > 0
150
+ ? Math.floor(skills.maxInjectSkills)
151
+ : 5;
152
+ const maxInjectChars = Number.isFinite(skills.maxInjectChars) && skills.maxInjectChars > 0
153
+ ? Math.floor(skills.maxInjectChars)
154
+ : 20_000;
155
+ this.skillsConfig = {
156
+ enabled: skills.enabled !== false,
157
+ paths,
158
+ maxPromptSkills,
159
+ maxInjectSkills,
160
+ maxInjectChars,
161
+ };
140
162
  this.modelLimits = runtime?.modelLimits;
141
163
  const baseCompaction = {
142
164
  auto: true,
143
165
  prune: true,
144
166
  pruneProtectTokens: 40_000,
145
167
  pruneMinimumTokens: 20_000,
168
+ toolOutputMode: 'afterToolCall',
146
169
  };
147
170
  const c = runtime?.compaction ?? {};
148
171
  this.compactionConfig = {
@@ -150,6 +173,7 @@ export class LingyunAgent {
150
173
  prune: c.prune ?? baseCompaction.prune,
151
174
  pruneProtectTokens: Math.max(0, c.pruneProtectTokens ?? baseCompaction.pruneProtectTokens),
152
175
  pruneMinimumTokens: Math.max(0, c.pruneMinimumTokens ?? baseCompaction.pruneMinimumTokens),
176
+ toolOutputMode: c.toolOutputMode ?? baseCompaction.toolOutputMode,
153
177
  };
154
178
  }
155
179
  updateConfig(config) {
@@ -686,11 +710,31 @@ export class LingyunAgent {
686
710
  });
687
711
  });
688
712
  }
689
- composeSystemPrompt(modelId) {
713
+ getSkillsPromptText() {
714
+ if (!this.skillsConfig.enabled)
715
+ return Promise.resolve(undefined);
716
+ if (this.skillsPromptTextPromise)
717
+ return this.skillsPromptTextPromise;
718
+ const { paths, maxPromptSkills } = this.skillsConfig;
719
+ this.skillsPromptTextPromise = getSkillIndex({
720
+ workspaceRoot: this.workspaceRoot,
721
+ searchPaths: paths,
722
+ allowExternalPaths: this.allowExternalPaths,
723
+ })
724
+ .then((index) => renderSkillsSectionForPrompt({
725
+ skills: index.skills,
726
+ maxSkills: maxPromptSkills,
727
+ workspaceRoot: this.workspaceRoot,
728
+ }))
729
+ .catch(() => undefined);
730
+ return this.skillsPromptTextPromise;
731
+ }
732
+ async composeSystemPrompt(modelId) {
690
733
  const basePrompt = this.config.systemPrompt || DEFAULT_SYSTEM_PROMPT;
691
- return this.plugins
692
- .trigger('experimental.chat.system.transform', { sessionId: this.config.sessionId, mode: this.getMode(), modelId }, { system: [basePrompt] })
693
- .then((out) => (Array.isArray(out.system) ? out.system.filter(Boolean) : [basePrompt]));
734
+ const skillsPromptText = await this.getSkillsPromptText();
735
+ const system = [basePrompt, skillsPromptText].filter(Boolean);
736
+ const out = await this.plugins.trigger('experimental.chat.system.transform', { sessionId: this.config.sessionId, mode: this.getMode(), modelId }, { system });
737
+ return Array.isArray(out.system) ? out.system.filter(Boolean) : system;
694
738
  }
695
739
  async compactSessionInternal(session, params, callbacks) {
696
740
  const maybeAwait = async (value) => {
@@ -721,7 +765,7 @@ export class LingyunAgent {
721
765
  middleware: [extractReasoningMiddleware({ tagName: 'think', startWithReasoning: false })],
722
766
  });
723
767
  const effective = getEffectiveHistory(session.history);
724
- const prepared = createHistoryForModel(effective);
768
+ const prepared = createHistoryForCompactionPrompt(effective, this.compactionConfig);
725
769
  const withoutIds = prepared.map(({ id: _id, ...rest }) => rest);
726
770
  const compactionUser = createUserHistoryMessage(promptText, { synthetic: true });
727
771
  const compactionModelMessages = await convertToModelMessages([...withoutIds, compactionUser], { tools: {} });
@@ -964,7 +1008,9 @@ export class LingyunAgent {
964
1008
  session.history.push(assistantMessage);
965
1009
  const lastAssistantText = getMessageText(assistantMessage).trim();
966
1010
  lastResponse = lastAssistantText || lastResponse;
967
- markPrunableToolOutputs(session.history, this.compactionConfig);
1011
+ if (this.compactionConfig.prune && this.compactionConfig.toolOutputMode === 'afterToolCall') {
1012
+ markPreviousAssistantToolOutputs(session.history);
1013
+ }
968
1014
  await Promise.resolve(callbacksSafe?.onIterationEnd?.(iteration));
969
1015
  const modelLimit = this.getModelLimit(modelId);
970
1016
  const reservedOutputTokens = getReservedOutputTokens({ modelLimit, maxOutputTokens: this.getMaxOutputTokens() });
@@ -990,6 +1036,77 @@ export class LingyunAgent {
990
1036
  callbacksSafe?.onComplete?.(lastResponse);
991
1037
  return lastResponse;
992
1038
  }
1039
+ async injectSkillsForUserText(session, text, callbacks, signal) {
1040
+ if (!this.skillsConfig.enabled)
1041
+ return;
1042
+ const mentions = extractSkillMentions(text);
1043
+ if (mentions.length === 0)
1044
+ return;
1045
+ const index = await getSkillIndex({
1046
+ workspaceRoot: this.workspaceRoot,
1047
+ searchPaths: this.skillsConfig.paths,
1048
+ allowExternalPaths: this.allowExternalPaths,
1049
+ signal,
1050
+ });
1051
+ const { selected, unknown } = selectSkillsForText(text, index);
1052
+ if (unknown.length > 0) {
1053
+ const availableSample = index.skills
1054
+ .map((s) => s.name)
1055
+ .slice(0, 20);
1056
+ const availableLabel = availableSample.length > 0
1057
+ ? ` Available: ${availableSample.map((n) => `$${n}`).join(', ')}${index.skills.length > availableSample.length ? ', ...' : ''}`
1058
+ : '';
1059
+ callbacks?.onNotice?.({
1060
+ level: 'warning',
1061
+ message: `Unknown skills: ${unknown.map((n) => `$${n}`).join(', ')}.${availableLabel}`,
1062
+ });
1063
+ }
1064
+ if (selected.length === 0)
1065
+ return;
1066
+ const maxSkills = this.skillsConfig.maxInjectSkills;
1067
+ const maxChars = this.skillsConfig.maxInjectChars;
1068
+ const selectedForInject = selected.slice(0, maxSkills);
1069
+ const activeLabel = selectedForInject.map((s) => `$${s.name}`).join(', ');
1070
+ const blocks = [];
1071
+ if (activeLabel) {
1072
+ blocks.push([
1073
+ '<skills>',
1074
+ `<active>${activeLabel}</active>`,
1075
+ 'You MUST apply ALL active skills for the next user request.',
1076
+ 'Treat skill instructions as additive. If they conflict, call it out and ask the user how to proceed (do not ignore a skill silently).',
1077
+ '</skills>',
1078
+ ].join('\n'));
1079
+ }
1080
+ for (const skill of selectedForInject) {
1081
+ if (signal?.aborted)
1082
+ break;
1083
+ let body;
1084
+ try {
1085
+ body = (await loadSkillFile(skill)).content;
1086
+ }
1087
+ catch {
1088
+ continue;
1089
+ }
1090
+ let truncated = false;
1091
+ if (body.length > maxChars) {
1092
+ body = body.slice(0, maxChars);
1093
+ truncated = true;
1094
+ }
1095
+ blocks.push([
1096
+ '<skill>',
1097
+ `<name>${skill.name}</name>`,
1098
+ `<path>${redactFsPathForPrompt(skill.filePath, { workspaceRoot: this.workspaceRoot })}</path>`,
1099
+ body.trimEnd(),
1100
+ truncated ? '\n\n... [TRUNCATED]' : '',
1101
+ '</skill>',
1102
+ ]
1103
+ .filter(Boolean)
1104
+ .join('\n'));
1105
+ }
1106
+ if (blocks.length > 0) {
1107
+ session.history.push(createUserHistoryMessage(blocks.join('\n\n'), { synthetic: true, skill: true }));
1108
+ }
1109
+ }
993
1110
  run(params) {
994
1111
  const queue = new AsyncQueue();
995
1112
  const callbacks = params.callbacks;
@@ -999,6 +1116,10 @@ export class LingyunAgent {
999
1116
  callbacks?.onDebug?.(message);
1000
1117
  queue.push({ type: 'debug', message });
1001
1118
  },
1119
+ onNotice: (notice) => {
1120
+ callbacks?.onNotice?.(notice);
1121
+ queue.push({ type: 'notice', notice });
1122
+ },
1002
1123
  onStatusChange: (status) => {
1003
1124
  callbacks?.onStatusChange?.(status);
1004
1125
  queue.push({ type: 'status', status: status });
@@ -1041,6 +1162,7 @@ export class LingyunAgent {
1041
1162
  };
1042
1163
  const done = (async () => {
1043
1164
  try {
1165
+ await this.injectSkillsForUserText(params.session, params.input, proxy, params.signal);
1044
1166
  params.session.history.push(createUserHistoryMessage(params.input));
1045
1167
  const text = await this.runOnce(params.session, proxy, params.signal);
1046
1168
  queue.push({ type: 'status', status: { type: 'done', message: '' } });
@@ -1054,6 +1176,9 @@ export class LingyunAgent {
1054
1176
  queue.fail(err);
1055
1177
  throw err;
1056
1178
  }
1179
+ finally {
1180
+ params.session.history = params.session.history.filter((msg) => !(msg.role === 'user' && msg.metadata?.skill));
1181
+ }
1057
1182
  })();
1058
1183
  return { events: queue, done };
1059
1184
  }