@qilin-agent/sidebar-opens 0.0.0-stage → 3.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.
- package/LICENSE +21 -0
- package/README.i18n.yaml +43 -0
- package/README.md +68 -2
- package/README.zh.md +73 -0
- package/lib/index.js +333 -0
- package/lib/typert.host.d.ts +3 -0
- package/lib/typert.host.js +99 -0
- package/lib/typert.remote-client.d.ts +24 -0
- package/lib/typert.remote-client.js +47 -0
- package/lib/types/index.d.ts +67 -0
- package/lib/types/index.js +313 -0
- package/lib/types/types.d.ts +17 -0
- package/lib/types/types.js +3 -0
- package/package.json +71 -3
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 kkutysllb
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.i18n.yaml
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# Bilingual-pair consistency record for README.md (docs/i18n/README.md): per heading
|
|
2
|
+
# section, a hash of its English and Chinese blocks outside code blocks and generated regions.
|
|
3
|
+
# After editing either side, bring the other along and re-record with:
|
|
4
|
+
# pnpm run verify-translation-pairing --write packages/host/sidebar-opens/README.md
|
|
5
|
+
/:
|
|
6
|
+
en: 96e86d8f1f2fd2f1
|
|
7
|
+
zh: 133353bfc33d5536
|
|
8
|
+
/qilin-agent-sidebar-opens:
|
|
9
|
+
en: 97611fa28977717d
|
|
10
|
+
zh: 1017316dca27570e
|
|
11
|
+
/qilin-agent-sidebar-opens/summary:
|
|
12
|
+
en: 96a42ba67afac8e4
|
|
13
|
+
zh: 243c7814e2a5bf9c
|
|
14
|
+
/qilin-agent-sidebar-opens/table-of-contents:
|
|
15
|
+
en: 368556c7719717f4
|
|
16
|
+
zh: 28c8e90d2b051204
|
|
17
|
+
/qilin-agent-sidebar-opens/the-tool:
|
|
18
|
+
en: dca03510399b377d
|
|
19
|
+
zh: 1a8f294023a8de74
|
|
20
|
+
/qilin-agent-sidebar-opens/delivery:
|
|
21
|
+
en: ae476c467a05ca97
|
|
22
|
+
zh: 35842db297712f12
|
|
23
|
+
/qilin-agent-sidebar-opens/model-experience:
|
|
24
|
+
en: 215e7ba838619b7b
|
|
25
|
+
zh: a311da3843709f90
|
|
26
|
+
/qilin-agent-sidebar-opens/model-experience/sidebar-open:
|
|
27
|
+
en: 5c385332963c706b
|
|
28
|
+
zh: 5c385332963c706b
|
|
29
|
+
/qilin-agent-sidebar-opens/model-experience/sidebar-open/what-the-model-sees:
|
|
30
|
+
en: 4b72f8439fbadea0
|
|
31
|
+
zh: ed9723a23ff7f0ab
|
|
32
|
+
/qilin-agent-sidebar-opens/model-experience/sidebar-open/token-effect:
|
|
33
|
+
en: 2f7bd9b9f7f01d36
|
|
34
|
+
zh: 669d25591716baab
|
|
35
|
+
/qilin-agent-sidebar-opens/model-experience/sidebar-open/kv-cache-effect:
|
|
36
|
+
en: 5d72f926299aaba9
|
|
37
|
+
zh: 684933cc36fe0ac6
|
|
38
|
+
/qilin-agent-sidebar-opens/known-limitations-and-deferred-work:
|
|
39
|
+
en: b703f660b903481d
|
|
40
|
+
zh: f453516d12a6f6bf
|
|
41
|
+
/qilin-agent-sidebar-opens/known-limitations-and-deferred-work/dev-note:
|
|
42
|
+
en: e67f633dc6fe7773
|
|
43
|
+
zh: 4e706f1b2b984e63
|
package/README.md
CHANGED
|
@@ -1,3 +1,69 @@
|
|
|
1
|
-
|
|
1
|
+
---
|
|
2
|
+
description: "Model-facing requests to open a file or an http(s) page in the Session's Sidebar."
|
|
3
|
+
kind: "package-reference"
|
|
4
|
+
---
|
|
2
5
|
|
|
3
|
-
|
|
6
|
+
# @qilin-agent/sidebar-opens
|
|
7
|
+
|
|
8
|
+
English | [中文](README.zh.md)
|
|
9
|
+
|
|
10
|
+
## Summary
|
|
11
|
+
|
|
12
|
+
`sidebar_open` lets the model show something instead of describing it: one existing file, or one http(s) page. The request travels as a Host Remote stream (`sidebarOpens.watch`) to [`@qilin-agent/client-ui-agent-opens`](../../client/ui-agent-opens/README.md), which opens the Session's own Sidebar on it. Delivery is transient: a request is consumed on send while a view is attached, and otherwise waits in a bounded per-Session queue a later attachment replays.
|
|
13
|
+
|
|
14
|
+
## Table of Contents
|
|
15
|
+
|
|
16
|
+
- [The tool](#the-tool)
|
|
17
|
+
- [Delivery](#delivery)
|
|
18
|
+
- [Model Experience](#model-experience)
|
|
19
|
+
- [Known Limitations and Deferred Work](#known-limitations-and-deferred-work)
|
|
20
|
+
- [Dev Note](#dev-note)
|
|
21
|
+
|
|
22
|
+
-----
|
|
23
|
+
|
|
24
|
+
## The tool
|
|
25
|
+
|
|
26
|
+
Exactly one of `path` or `url` is required.
|
|
27
|
+
|
|
28
|
+
- `path` accepts an absolute path or one relative to the Session working directory, and is verified through the Session filesystem before the request is made: a path that does not resolve, or that names something other than a regular file, fails the call rather than opening a broken tab.
|
|
29
|
+
- `url` accepts `http://` and `https://` only. Anything else is refused, so a `file://` the model composed cannot reach the browser's opener.
|
|
30
|
+
- The title is the file's basename or the page's host.
|
|
31
|
+
|
|
32
|
+
The result reports the resolved target and whether an attached view took it now. The Host-minted request identity stays internal.
|
|
33
|
+
|
|
34
|
+
## Delivery
|
|
35
|
+
|
|
36
|
+
`watch(sessionId, signal)` yields what queued while nothing was attached and then every request as it arrives. A second watcher for the same Session takes over: the newest view is the one the user is looking at. `enqueue` answers whether an attached view consumed the request; with none it queues, dropping the oldest request past `maxQueued` so a Session nobody watches cannot accumulate requests without end.
|
|
37
|
+
|
|
38
|
+
## Model Experience
|
|
39
|
+
|
|
40
|
+
### sidebar_open
|
|
41
|
+
|
|
42
|
+
#### What the model sees
|
|
43
|
+
|
|
44
|
+
The [sidebar_open schema](../../../docs/tool-catalog.md#sidebar_open) offers one file or one http(s) page: "Open one file or one http(s) page in the Sidebar the user is viewing this Session in. Use it when the user asked to see something: a file you produced, a file worth reading beside the conversation, or a page you found. Pass exactly one of `path` (a file that already exists) or `url`. The file opens in the document preview and the page in the built-in browser; both appear beside the conversation rather than leaving the application." Results are one line — the target opened, or the target waiting for the Session's Sidebar — with `kind`, `target`, `title`, and `delivered`.
|
|
45
|
+
|
|
46
|
+
#### Token effect
|
|
47
|
+
|
|
48
|
+
Two parameters and one small result per call. Nothing here is added to the system prompt.
|
|
49
|
+
|
|
50
|
+
#### KV Cache effect
|
|
51
|
+
|
|
52
|
+
None directly. A call and its result append to the Session like any other tool turn; the cache moves the way it does for any tool call.
|
|
53
|
+
|
|
54
|
+
## Known Limitations and Deferred Work
|
|
55
|
+
|
|
56
|
+
- **Folders are not openable.** The Sidebar's file tree is rooted at the workspace, so "show me this directory" has no tab to reach; only a regular file or a page can be opened. A folder path fails the call.
|
|
57
|
+
- **A page is only as good as the composing browser tab type.** With `@qilin-agent/client-ui-sidebar-browser` composed out, the consumer falls back to a new browser tab, which leaves the application.
|
|
58
|
+
- **Requests do not survive a reload.** A request made while no view was attached waits only in the Host process's memory for that Session; restarting the Host or disposing the Session drops it.
|
|
59
|
+
- **The queue is bounded and drops oldest-first.** A Session whose sidebar is never shown keeps only its most recent `maxQueued` requests.
|
|
60
|
+
|
|
61
|
+
<a id="dev-note"></a>
|
|
62
|
+
### Dev Note
|
|
63
|
+
|
|
64
|
+
<details>
|
|
65
|
+
<summary>Working context for maintainers — click to expand</summary>
|
|
66
|
+
|
|
67
|
+
None.
|
|
68
|
+
|
|
69
|
+
</details>
|
package/README.zh.md
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: "面向模型的请求:在所属会话的侧栏中打开一个文件或一个 http(s) 页面。"
|
|
3
|
+
kind: "package-reference"
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# @qilin-agent/sidebar-opens
|
|
7
|
+
|
|
8
|
+
[English](README.md) | 中文
|
|
9
|
+
|
|
10
|
+
## 概述
|
|
11
|
+
|
|
12
|
+
`sidebar_open` 让模型直接"展示"而不是"描述":一个已存在的文件,或一个 http(s) 页面。请求以 Host Remote 流(`sidebarOpens.watch`)送到 [`@qilin-agent/client-ui-agent-opens`](../../client/ui-agent-opens/README.zh.md),由它在**该会话自己的**侧栏中打开。投递是瞬时的:视图附着时随发随取,否则在每会话的有界队列里等待下一次附着重放。
|
|
13
|
+
|
|
14
|
+
## 目录
|
|
15
|
+
|
|
16
|
+
- [工具](#the-tool)
|
|
17
|
+
- [投递](#delivery)
|
|
18
|
+
- [模型体验](#model-experience)
|
|
19
|
+
- [已知限制与后续工作](#known-limitations-and-deferred-work)
|
|
20
|
+
- [开发备注](#dev-note)
|
|
21
|
+
|
|
22
|
+
-----
|
|
23
|
+
|
|
24
|
+
<a id="the-tool"></a>
|
|
25
|
+
## 工具
|
|
26
|
+
|
|
27
|
+
`path` 与 `url` **必须且只能提供一个**。
|
|
28
|
+
|
|
29
|
+
- `path` 接受绝对路径或相对会话工作目录的路径,并在请求发出前经会话文件系统验证:无法解析、或不是普通文件时,调用本身失败,而不是打开一个坏 tab。
|
|
30
|
+
- `url` 只接受 `http://` 与 `https://`。其他一律拒绝,因此模型拼出的 `file://` 到不了浏览器的打开入口。
|
|
31
|
+
- 标题取文件的 basename 或页面的 host。
|
|
32
|
+
|
|
33
|
+
结果报告解析后的目标,以及附着的视图是否当场取走它。Host 铸造的请求标识留在内部。
|
|
34
|
+
|
|
35
|
+
<a id="delivery"></a>
|
|
36
|
+
## 投递
|
|
37
|
+
|
|
38
|
+
`watch(sessionId, signal)` 先产出"无人附着期间排队的请求",再产出随后到达的每一个。同一会话的第二个 watcher 会**接管**:最新的视图就是用户正在看的那个。`enqueue` 回答附着的视图是否当场消费;无人附着时入队,并在超过 `maxQueued` 时丢弃最旧的,避免无人观看的会话无限堆积。
|
|
39
|
+
|
|
40
|
+
<a id="model-experience"></a>
|
|
41
|
+
## 模型体验
|
|
42
|
+
|
|
43
|
+
### sidebar_open
|
|
44
|
+
|
|
45
|
+
#### 模型看到什么
|
|
46
|
+
|
|
47
|
+
[sidebar_open 的 schema](../../../docs/tool-catalog.zh.md#sidebar_open) 提供一个文件或一个 http(s) 页面:"Open one file or one http(s) page in the Sidebar the user is viewing this Session in. Use it when the user asked to see something: a file you produced, a file worth reading beside the conversation, or a page you found. Pass exactly one of `path` (a file that already exists) or `url`. The file opens in the document preview and the page in the built-in browser; both appear beside the conversation rather than leaving the application." 结果是一行——目标已打开,或目标在等待该会话的侧栏——并带有 `kind`、`target`、`title` 与 `delivered`。
|
|
48
|
+
|
|
49
|
+
#### Token 影响
|
|
50
|
+
|
|
51
|
+
每次调用两个参数与一个很小的结果。这里没有任何内容进入系统提示词。
|
|
52
|
+
|
|
53
|
+
#### KV Cache 影响
|
|
54
|
+
|
|
55
|
+
没有直接影响。一次调用及其结果像任何工具轮次一样追加到会话;缓存随普通工具调用的方式变化。
|
|
56
|
+
|
|
57
|
+
<a id="known-limitations-and-deferred-work"></a>
|
|
58
|
+
## 已知限制与后续工作
|
|
59
|
+
|
|
60
|
+
- **目录不可打开。** 侧栏的文件树以工作区为根,"给我看这个目录"没有可到达的 tab;只有普通文件或页面可以打开。目录路径会让调用失败。
|
|
61
|
+
- **页面的落点取决于组合进来的浏览器 tab 类型。** 把 `@qilin-agent/client-ui-sidebar-browser` 组合掉时,消费者退回新浏览器标签页,即离开应用。
|
|
62
|
+
- **请求不跨重新加载。** 无人附着时发出的请求只在该 Host 进程内存中等待,直到该会话结束;重启 Host 或销毁会话即丢弃。
|
|
63
|
+
- **队列有界且丢弃最旧。** 从不开侧栏的会话只保留最近 `maxQueued` 个请求。
|
|
64
|
+
|
|
65
|
+
<a id="dev-note"></a>
|
|
66
|
+
### 开发备注
|
|
67
|
+
|
|
68
|
+
<details>
|
|
69
|
+
<summary>维护者信息 — 点击展开</summary>
|
|
70
|
+
|
|
71
|
+
无。
|
|
72
|
+
|
|
73
|
+
</details>
|
package/lib/index.js
ADDED
|
@@ -0,0 +1,333 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { basename } from "node:path";
|
|
3
|
+
import z from "@qilin-agent/schemastery";
|
|
4
|
+
import { defineTool } from "@qilin-agent/tools";
|
|
5
|
+
import { Remote, TypertRemoteService } from "@qilin-agent/typert-protocol";
|
|
6
|
+
//#region lib/types/index.js
|
|
7
|
+
/**
|
|
8
|
+
* `sidebar_open`: the model-facing request to show a file or an http(s) page in
|
|
9
|
+
* the Sidebar of its own Session.
|
|
10
|
+
*
|
|
11
|
+
* Delivery is a Host-to-browser push over a Remote stream (`watch`): a request
|
|
12
|
+
* is consumed on send while a view for that Session is attached, and otherwise
|
|
13
|
+
* waits in a bounded per-Session queue that a later attachment replays. A
|
|
14
|
+
* request is never durable: replaying a historical open on every page load
|
|
15
|
+
* would reopen a tab the user closed, which is why this is a stream and not a
|
|
16
|
+
* Session event. The tool call and its result stay in the Session log like any
|
|
17
|
+
* other tool, so what the model asked for is still reconstructable.
|
|
18
|
+
*/
|
|
19
|
+
var __runInitializers = function(thisArg, initializers, value) {
|
|
20
|
+
var useValue = arguments.length > 2;
|
|
21
|
+
for (var i = 0; i < initializers.length; i++) value = useValue ? initializers[i].call(thisArg, value) : initializers[i].call(thisArg);
|
|
22
|
+
return useValue ? value : void 0;
|
|
23
|
+
};
|
|
24
|
+
var __esDecorate = function(ctor, descriptorIn, decorators, contextIn, initializers, extraInitializers) {
|
|
25
|
+
function accept(f) {
|
|
26
|
+
if (f !== void 0 && typeof f !== "function") throw new TypeError("Function expected");
|
|
27
|
+
return f;
|
|
28
|
+
}
|
|
29
|
+
var kind = contextIn.kind, key = kind === "getter" ? "get" : kind === "setter" ? "set" : "value";
|
|
30
|
+
var target = !descriptorIn && ctor ? contextIn["static"] ? ctor : ctor.prototype : null;
|
|
31
|
+
var descriptor = descriptorIn || (target ? Object.getOwnPropertyDescriptor(target, contextIn.name) : {});
|
|
32
|
+
var _, done = false;
|
|
33
|
+
for (var i = decorators.length - 1; i >= 0; i--) {
|
|
34
|
+
var context = {};
|
|
35
|
+
for (var p in contextIn) context[p] = p === "access" ? {} : contextIn[p];
|
|
36
|
+
for (var p in contextIn.access) context.access[p] = contextIn.access[p];
|
|
37
|
+
context.addInitializer = function(f) {
|
|
38
|
+
if (done) throw new TypeError("Cannot add initializers after decoration has completed");
|
|
39
|
+
extraInitializers.push(accept(f || null));
|
|
40
|
+
};
|
|
41
|
+
var result = (0, decorators[i])(kind === "accessor" ? {
|
|
42
|
+
get: descriptor.get,
|
|
43
|
+
set: descriptor.set
|
|
44
|
+
} : descriptor[key], context);
|
|
45
|
+
if (kind === "accessor") {
|
|
46
|
+
if (result === void 0) continue;
|
|
47
|
+
if (result === null || typeof result !== "object") throw new TypeError("Object expected");
|
|
48
|
+
if (_ = accept(result.get)) descriptor.get = _;
|
|
49
|
+
if (_ = accept(result.set)) descriptor.set = _;
|
|
50
|
+
if (_ = accept(result.init)) initializers.unshift(_);
|
|
51
|
+
} else if (_ = accept(result)) if (kind === "field") initializers.unshift(_);
|
|
52
|
+
else descriptor[key] = _;
|
|
53
|
+
}
|
|
54
|
+
if (target) Object.defineProperty(target, contextIn.name, descriptor);
|
|
55
|
+
done = true;
|
|
56
|
+
};
|
|
57
|
+
/** Stable Loader identity. */
|
|
58
|
+
const name = "sidebar-opens";
|
|
59
|
+
/** Services the tool verifies its target through. */
|
|
60
|
+
const inject = ["tools", "fs"];
|
|
61
|
+
/** Schemastery validation for {@link Config}. */
|
|
62
|
+
const Config = z.object({ maxQueued: z.number().default(16) });
|
|
63
|
+
/** One live watcher's inbox: pushed requests, and the wait a generator parks on. */
|
|
64
|
+
var Inbox = class {
|
|
65
|
+
items = [];
|
|
66
|
+
wake;
|
|
67
|
+
closed = false;
|
|
68
|
+
/**
|
|
69
|
+
* @param request - the next request for the watcher.
|
|
70
|
+
*/
|
|
71
|
+
push(request) {
|
|
72
|
+
this.items.push(request);
|
|
73
|
+
this.wake?.();
|
|
74
|
+
this.wake = void 0;
|
|
75
|
+
}
|
|
76
|
+
/** Stop delivering: the parked wait resolves empty and later pushes are dropped. */
|
|
77
|
+
close() {
|
|
78
|
+
this.closed = true;
|
|
79
|
+
this.wake?.();
|
|
80
|
+
this.wake = void 0;
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* The next request, the moment one arrives.
|
|
84
|
+
*
|
|
85
|
+
* A parked wait is released by {@link close}, which a watcher's own abort
|
|
86
|
+
* handler calls, so this adds no listener of its own: one per wait would
|
|
87
|
+
* accumulate on the transport's signal for the whole life of the watch.
|
|
88
|
+
* @returns the request, or undefined once the watcher is closed.
|
|
89
|
+
*/
|
|
90
|
+
async next() {
|
|
91
|
+
while (this.items.length === 0) {
|
|
92
|
+
if (this.closed) return void 0;
|
|
93
|
+
await new Promise((resolve) => {
|
|
94
|
+
this.wake = resolve;
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
return this.items.shift();
|
|
98
|
+
}
|
|
99
|
+
};
|
|
100
|
+
/**
|
|
101
|
+
* Per-Session open queues with their attached watchers.
|
|
102
|
+
*
|
|
103
|
+
* One request has one destination: with a watcher attached it is pushed there
|
|
104
|
+
* and forgotten, and with none it waits in the Session's queue until a view
|
|
105
|
+
* attaches. The queue is bounded because a Session nobody is watching must not
|
|
106
|
+
* accumulate requests without end.
|
|
107
|
+
*/
|
|
108
|
+
let SidebarOpens = (() => {
|
|
109
|
+
let _classSuper = TypertRemoteService;
|
|
110
|
+
let _instanceExtraInitializers = [];
|
|
111
|
+
let _watch_decorators;
|
|
112
|
+
return class SidebarOpens extends _classSuper {
|
|
113
|
+
static {
|
|
114
|
+
const _metadata = typeof Symbol === "function" && Symbol.metadata ? Object.create(_classSuper[Symbol.metadata] ?? null) : void 0;
|
|
115
|
+
_watch_decorators = [Remote({ mode: "stream" })];
|
|
116
|
+
__esDecorate(this, null, _watch_decorators, {
|
|
117
|
+
kind: "method",
|
|
118
|
+
name: "watch",
|
|
119
|
+
static: false,
|
|
120
|
+
private: false,
|
|
121
|
+
access: {
|
|
122
|
+
has: (obj) => "watch" in obj,
|
|
123
|
+
get: (obj) => obj.watch
|
|
124
|
+
},
|
|
125
|
+
metadata: _metadata
|
|
126
|
+
}, null, _instanceExtraInitializers);
|
|
127
|
+
if (_metadata) Object.defineProperty(this, Symbol.metadata, {
|
|
128
|
+
enumerable: true,
|
|
129
|
+
configurable: true,
|
|
130
|
+
writable: true,
|
|
131
|
+
value: _metadata
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
maxQueued = __runInitializers(this, _instanceExtraInitializers);
|
|
135
|
+
pending = /* @__PURE__ */ new Map();
|
|
136
|
+
watchers = /* @__PURE__ */ new Map();
|
|
137
|
+
/**
|
|
138
|
+
* @param ctx - owning Host context.
|
|
139
|
+
* @param maxQueued - requests one Session may queue while no view is attached.
|
|
140
|
+
*/
|
|
141
|
+
constructor(ctx, maxQueued) {
|
|
142
|
+
super(ctx, "sidebarOpens");
|
|
143
|
+
this.maxQueued = maxQueued;
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* Deliver one request to the Session's view, or queue it for the next one.
|
|
147
|
+
* @param sessionId - the Session whose Sidebar the request targets.
|
|
148
|
+
* @param request - the resolved request.
|
|
149
|
+
* @returns whether an attached view consumed it now.
|
|
150
|
+
*/
|
|
151
|
+
enqueue(sessionId, request) {
|
|
152
|
+
const watcher = this.watchers.get(sessionId);
|
|
153
|
+
if (watcher !== void 0) {
|
|
154
|
+
watcher.push(request);
|
|
155
|
+
return true;
|
|
156
|
+
}
|
|
157
|
+
const queue = this.pending.get(sessionId);
|
|
158
|
+
if (queue === void 0) {
|
|
159
|
+
this.pending.set(sessionId, [request]);
|
|
160
|
+
return false;
|
|
161
|
+
}
|
|
162
|
+
queue.push(request);
|
|
163
|
+
if (queue.length > this.maxQueued) queue.shift();
|
|
164
|
+
return false;
|
|
165
|
+
}
|
|
166
|
+
/**
|
|
167
|
+
* Watch one Session's opens: what queued while nothing was attached, then
|
|
168
|
+
* every request as it arrives.
|
|
169
|
+
* @param sessionId - the Session whose Sidebar is watching.
|
|
170
|
+
* @param signal - physical Remote stream cancellation.
|
|
171
|
+
* @returns the queued requests followed by the live ones.
|
|
172
|
+
*/
|
|
173
|
+
async *watch(sessionId, signal) {
|
|
174
|
+
const queued = this.pending.get(sessionId) ?? [];
|
|
175
|
+
this.pending.delete(sessionId);
|
|
176
|
+
const inbox = new Inbox();
|
|
177
|
+
this.watchers.get(sessionId)?.close();
|
|
178
|
+
this.watchers.set(sessionId, inbox);
|
|
179
|
+
const release = () => {
|
|
180
|
+
if (this.watchers.get(sessionId) === inbox) this.watchers.delete(sessionId);
|
|
181
|
+
inbox.close();
|
|
182
|
+
};
|
|
183
|
+
signal.addEventListener("abort", release, { once: true });
|
|
184
|
+
try {
|
|
185
|
+
for (const request of queued) {
|
|
186
|
+
if (signal.aborted) return;
|
|
187
|
+
yield request;
|
|
188
|
+
}
|
|
189
|
+
while (!signal.aborted) {
|
|
190
|
+
const request = await inbox.next();
|
|
191
|
+
if (request === void 0) return;
|
|
192
|
+
yield request;
|
|
193
|
+
}
|
|
194
|
+
} finally {
|
|
195
|
+
signal.removeEventListener("abort", release);
|
|
196
|
+
release();
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
/**
|
|
200
|
+
* Drop every queue, for a Host that is going away.
|
|
201
|
+
*/
|
|
202
|
+
dispose() {
|
|
203
|
+
this.pending.clear();
|
|
204
|
+
for (const inbox of this.watchers.values()) inbox.close();
|
|
205
|
+
this.watchers.clear();
|
|
206
|
+
}
|
|
207
|
+
};
|
|
208
|
+
})();
|
|
209
|
+
/** The exact http(s) URL a request may open. */
|
|
210
|
+
function pageUrl(value) {
|
|
211
|
+
let url;
|
|
212
|
+
try {
|
|
213
|
+
url = new URL(value);
|
|
214
|
+
} catch {
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
return url.protocol === "http:" || url.protocol === "https:" ? url.toString() : void 0;
|
|
218
|
+
}
|
|
219
|
+
/**
|
|
220
|
+
* Register the `sidebar_open` tool and its delivery service.
|
|
221
|
+
* @param ctx - Host context with the tool registry and the filesystem.
|
|
222
|
+
* @param config - validated queue bound.
|
|
223
|
+
*/
|
|
224
|
+
function apply(ctx, config) {
|
|
225
|
+
if (!Number.isSafeInteger(config.maxQueued) || config.maxQueued < 1) throw new Error("sidebar-opens requires a positive integer maxQueued");
|
|
226
|
+
const opens = new SidebarOpens(ctx, config.maxQueued);
|
|
227
|
+
ctx.effect(() => () => {
|
|
228
|
+
opens.dispose();
|
|
229
|
+
}, "sidebar-opens: queues");
|
|
230
|
+
ctx.tools.register(defineTool({
|
|
231
|
+
name: "sidebar_open",
|
|
232
|
+
description: "Open one file or one http(s) page in the Sidebar the user is viewing this Session in. Use it when the user asked to see something: a file you produced, a file worth reading beside the conversation, or a page you found. Pass exactly one of `path` (a file that already exists) or `url`. The file opens in the document preview and the page in the built-in browser; both appear beside the conversation rather than leaving the application.",
|
|
233
|
+
parameters: {
|
|
234
|
+
path: {
|
|
235
|
+
type: "string",
|
|
236
|
+
description: "Path of an existing regular file, relative to the Session working directory or absolute."
|
|
237
|
+
},
|
|
238
|
+
url: {
|
|
239
|
+
type: "string",
|
|
240
|
+
description: "An http:// or https:// page to open in the built-in browser."
|
|
241
|
+
}
|
|
242
|
+
},
|
|
243
|
+
output: {
|
|
244
|
+
schema: {
|
|
245
|
+
type: "object",
|
|
246
|
+
additionalProperties: false,
|
|
247
|
+
properties: {
|
|
248
|
+
kind: {
|
|
249
|
+
type: "string",
|
|
250
|
+
required: true,
|
|
251
|
+
enum: ["file", "url"]
|
|
252
|
+
},
|
|
253
|
+
target: {
|
|
254
|
+
type: "string",
|
|
255
|
+
required: true
|
|
256
|
+
},
|
|
257
|
+
title: {
|
|
258
|
+
type: "string",
|
|
259
|
+
required: true
|
|
260
|
+
},
|
|
261
|
+
delivered: {
|
|
262
|
+
type: "boolean",
|
|
263
|
+
required: true
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
},
|
|
267
|
+
render: (_args, value) => [{
|
|
268
|
+
type: "text",
|
|
269
|
+
text: value.delivered ? `Opened ${value.target} in the sidebar.` : `Queued ${value.target} to open in the sidebar as soon as that Session's sidebar is shown.`
|
|
270
|
+
}]
|
|
271
|
+
},
|
|
272
|
+
async execute(args, exec) {
|
|
273
|
+
if (exec.agent === void 0) throw new Error("sidebar_open requires an initiating agent");
|
|
274
|
+
const hasPath = args.path !== void 0 && args.path.trim().length > 0;
|
|
275
|
+
const hasUrl = args.url !== void 0 && args.url.trim().length > 0;
|
|
276
|
+
if (hasPath === hasUrl) throw new Error("sidebar_open requires exactly one of path or url");
|
|
277
|
+
const session = exec.agent.session;
|
|
278
|
+
const request = hasUrl ? urlRequest(args.url) : await fileRequest(ctx, exec.signal, session.header.cwd, args.path);
|
|
279
|
+
return {
|
|
280
|
+
kind: request.kind,
|
|
281
|
+
target: request.target,
|
|
282
|
+
title: request.title,
|
|
283
|
+
delivered: opens.enqueue(session.id, request)
|
|
284
|
+
};
|
|
285
|
+
}
|
|
286
|
+
}));
|
|
287
|
+
}
|
|
288
|
+
/**
|
|
289
|
+
* One page request from the model's URL.
|
|
290
|
+
* @param value - the URL as the model wrote it.
|
|
291
|
+
* @returns the request to deliver.
|
|
292
|
+
* @throws when the URL is malformed or is not http(s).
|
|
293
|
+
*/
|
|
294
|
+
function urlRequest(value) {
|
|
295
|
+
const target = pageUrl(value);
|
|
296
|
+
if (target === void 0) throw new Error("sidebar_open only opens http:// and https:// URLs; pass a path for a local file");
|
|
297
|
+
return {
|
|
298
|
+
id: randomUUID(),
|
|
299
|
+
kind: "url",
|
|
300
|
+
target,
|
|
301
|
+
title: new URL(target).host
|
|
302
|
+
};
|
|
303
|
+
}
|
|
304
|
+
/**
|
|
305
|
+
* One file request, verified through the Session filesystem before it is made
|
|
306
|
+
* so the browser never opens a path that does not resolve.
|
|
307
|
+
* @param ctx - Host context carrying the filesystem.
|
|
308
|
+
* @param signal - tool cancellation.
|
|
309
|
+
* @param cwd - the Session working directory relative paths resolve against.
|
|
310
|
+
* @param value - the path as the model wrote it.
|
|
311
|
+
* @returns the request to deliver.
|
|
312
|
+
* @throws when the path does not name a regular file.
|
|
313
|
+
*/
|
|
314
|
+
async function fileRequest(ctx, signal, cwd, value) {
|
|
315
|
+
signal.throwIfAborted();
|
|
316
|
+
const target = await ctx.fs.resolve(value, {
|
|
317
|
+
...cwd === void 0 ? {} : { cwd },
|
|
318
|
+
signal
|
|
319
|
+
});
|
|
320
|
+
const info = await ctx.fs.stat(target, signal);
|
|
321
|
+
if (info === void 0) throw new Error(`Cannot open ${value}: file not found. Check the path and retry.`);
|
|
322
|
+
if (info.type !== "file") throw new Error(`Cannot open ${value}: not a regular file`);
|
|
323
|
+
signal.throwIfAborted();
|
|
324
|
+
const path = ctx.fs.processPath(target);
|
|
325
|
+
return {
|
|
326
|
+
id: randomUUID(),
|
|
327
|
+
kind: "file",
|
|
328
|
+
target: path,
|
|
329
|
+
title: basename(path)
|
|
330
|
+
};
|
|
331
|
+
}
|
|
332
|
+
//#endregion
|
|
333
|
+
export { Config, SidebarOpens, apply, inject, name };
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
/* Generated by @qilin-agent/typert-generator from FaceModel — do not edit. */
|
|
2
|
+
import { z } from 'zod'
|
|
3
|
+
|
|
4
|
+
let _qilin_agent_sidebar_opens_sidebarOpens_watch_parameter_0$schema$value
|
|
5
|
+
const _qilin_agent_sidebar_opens_sidebarOpens_watch_parameter_0$schema = () => (_qilin_agent_sidebar_opens_sidebarOpens_watch_parameter_0$schema$value ??= z.intersection(z.string(), z.unknown()))
|
|
6
|
+
let _qilin_agent_sidebar_opens_sidebarOpens_watch_result$schema$value
|
|
7
|
+
const _qilin_agent_sidebar_opens_sidebarOpens_watch_result$schema = () => (_qilin_agent_sidebar_opens_sidebarOpens_watch_result$schema$value ??= z.object({
|
|
8
|
+
'id': z.string().readonly(),
|
|
9
|
+
'kind': z.union([z.literal("file"), z.literal("url")]).readonly(),
|
|
10
|
+
'target': z.string().readonly(),
|
|
11
|
+
'title': z.string().readonly(),
|
|
12
|
+
}))
|
|
13
|
+
|
|
14
|
+
export const TYPERT = {
|
|
15
|
+
package: '@qilin-agent/sidebar-opens',
|
|
16
|
+
face: 'host',
|
|
17
|
+
schemas: [
|
|
18
|
+
],
|
|
19
|
+
invocations: [
|
|
20
|
+
{
|
|
21
|
+
id: '@qilin-agent/sidebar-opens#sidebarOpens/watch',
|
|
22
|
+
service: 'sidebarOpens',
|
|
23
|
+
namespace: 'sidebarOpens',
|
|
24
|
+
method: 'watch',
|
|
25
|
+
mode: 'stream',
|
|
26
|
+
invocation: { kind: 'direct' },
|
|
27
|
+
parameters: [
|
|
28
|
+
{
|
|
29
|
+
name: 'sessionId',
|
|
30
|
+
wire: 'sessionId',
|
|
31
|
+
source: 'json',
|
|
32
|
+
codec: {
|
|
33
|
+
mode: 'strict',
|
|
34
|
+
typeSymbol: '@qilin-agent/session/types#SessionId',
|
|
35
|
+
create: _qilin_agent_sidebar_opens_sidebarOpens_watch_parameter_0$schema,
|
|
36
|
+
},
|
|
37
|
+
},
|
|
38
|
+
],
|
|
39
|
+
cancellation: { parameter: 'signal' },
|
|
40
|
+
result: {
|
|
41
|
+
mode: 'strict',
|
|
42
|
+
typeSymbol: '@qilin-agent/sidebar-opens/types#SidebarOpenRequest',
|
|
43
|
+
create: _qilin_agent_sidebar_opens_sidebarOpens_watch_result$schema,
|
|
44
|
+
},
|
|
45
|
+
sourceLocation: {"file":"packages/host/sidebar-opens/src/index.ts","line":143,"column":10},
|
|
46
|
+
},
|
|
47
|
+
],
|
|
48
|
+
model: {
|
|
49
|
+
"services": [
|
|
50
|
+
{
|
|
51
|
+
"description": "Per-Session open queues with their attached watchers. One request has one destination: with a watcher attached it is pushed there and forgotten, and with none it waits in the Session's queue until a view attaches. The queue is bounded because a Session nobody is watching must not accumulate requests without end.",
|
|
52
|
+
"summary": "Per-Session open queues with their attached watchers.",
|
|
53
|
+
"tags": [],
|
|
54
|
+
"jsDoc": "/**\n * Per-Session open queues with their attached watchers.\n *\n * One request has one destination: with a watcher attached it is pushed there\n * and forgotten, and with none it waits in the Session's queue until a view\n * attaches. The queue is bounded because a Session nobody is watching must not\n * accumulate requests without end.\n */",
|
|
55
|
+
"key": "sidebarOpens",
|
|
56
|
+
"exportName": "SidebarOpens",
|
|
57
|
+
"members": [
|
|
58
|
+
{
|
|
59
|
+
"kind": "method",
|
|
60
|
+
"name": "enqueue",
|
|
61
|
+
"signature": "enqueue(sessionId: SessionId, request: SidebarOpenRequest): boolean",
|
|
62
|
+
"summary": "Deliver one request to the Session's view, or queue it for the next one.",
|
|
63
|
+
"jsDoc": "/**\n * Deliver one request to the Session's view, or queue it for the next one.\n * @param sessionId - the Session whose Sidebar the request targets.\n * @param request - the resolved request.\n * @returns whether an attached view consumed it now.\n */"
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
"kind": "method",
|
|
67
|
+
"name": "watch",
|
|
68
|
+
"signature": "@Remote({ mode: 'stream' }) async *watch(sessionId: SessionId, signal: AbortSignal): AsyncIterable<SidebarOpenRequest>",
|
|
69
|
+
"summary": "Watch one Session's opens: what queued while nothing was attached, then every request as it arrives.",
|
|
70
|
+
"jsDoc": "/**\n * Watch one Session's opens: what queued while nothing was attached, then\n * every request as it arrives.\n * @param sessionId - the Session whose Sidebar is watching.\n * @param signal - physical Remote stream cancellation.\n * @returns the queued requests followed by the live ones.\n */"
|
|
71
|
+
},
|
|
72
|
+
{
|
|
73
|
+
"kind": "method",
|
|
74
|
+
"name": "dispose",
|
|
75
|
+
"signature": "dispose(): void",
|
|
76
|
+
"summary": "Drop every queue, for a Host that is going away.",
|
|
77
|
+
"jsDoc": "/**\n * Drop every queue, for a Host that is going away.\n */"
|
|
78
|
+
}
|
|
79
|
+
],
|
|
80
|
+
"types": [
|
|
81
|
+
{
|
|
82
|
+
"name": "Branded",
|
|
83
|
+
"declaration": "export type Branded<B extends string> = string & { readonly [BRAND]: B; };"
|
|
84
|
+
},
|
|
85
|
+
{
|
|
86
|
+
"name": "SessionId",
|
|
87
|
+
"declaration": "export type SessionId = Branded<'SessionId'>;"
|
|
88
|
+
},
|
|
89
|
+
{
|
|
90
|
+
"name": "SidebarOpenRequest",
|
|
91
|
+
"declaration": "export interface SidebarOpenRequest {\n readonly id: string;\n readonly kind: 'file' | 'url';\n readonly target: string;\n readonly title: string;\n}"
|
|
92
|
+
}
|
|
93
|
+
]
|
|
94
|
+
}
|
|
95
|
+
],
|
|
96
|
+
"events": [],
|
|
97
|
+
"objects": []
|
|
98
|
+
},
|
|
99
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/* Generated by @qilin-agent/typert-generator from the Host FaceModel — do not edit. */
|
|
2
|
+
import type {
|
|
3
|
+
RemoteResult,
|
|
4
|
+
RemoteStreamHandle,
|
|
5
|
+
TypertRemoteContribution,
|
|
6
|
+
} from '@qilin-agent/typert-protocol'
|
|
7
|
+
import type { SessionId } from '@qilin-agent/session/types'
|
|
8
|
+
import type { SidebarOpenRequest } from '@qilin-agent/sidebar-opens/types'
|
|
9
|
+
|
|
10
|
+
declare module '@qilin-agent/typert-protocol' {
|
|
11
|
+
interface TypertRemoteNamespace$736964656261724f70656e73 {
|
|
12
|
+
watch: (sessionId: SessionId, signal?: AbortSignal) => RemoteStreamHandle<SidebarOpenRequest, never>
|
|
13
|
+
}
|
|
14
|
+
interface TypertRemoteMap {
|
|
15
|
+
'sidebarOpens/watch': (sessionId: SessionId, signal?: AbortSignal) => RemoteStreamHandle<SidebarOpenRequest, never>
|
|
16
|
+
}
|
|
17
|
+
interface TypertRemoteNamespaceMap {
|
|
18
|
+
'sidebarOpens': TypertRemoteNamespace$736964656261724f70656e73
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export declare const TYPERT_REMOTE: TypertRemoteContribution
|
|
23
|
+
export default TYPERT_REMOTE
|
|
24
|
+
//# sourceMappingURL=typert.remote-client.d.ts.map
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/* Generated by @qilin-agent/typert-generator from the Host FaceModel — do not edit. */
|
|
2
|
+
import { z } from 'zod'
|
|
3
|
+
|
|
4
|
+
let _qilin_agent_sidebar_opens_sidebarOpens_watch_parameter_0$schema$value
|
|
5
|
+
const _qilin_agent_sidebar_opens_sidebarOpens_watch_parameter_0$schema = () => (_qilin_agent_sidebar_opens_sidebarOpens_watch_parameter_0$schema$value ??= z.intersection(z.string(), z.unknown()))
|
|
6
|
+
let _qilin_agent_sidebar_opens_sidebarOpens_watch_result$schema$value
|
|
7
|
+
const _qilin_agent_sidebar_opens_sidebarOpens_watch_result$schema = () => (_qilin_agent_sidebar_opens_sidebarOpens_watch_result$schema$value ??= z.object({
|
|
8
|
+
'id': z.string().readonly(),
|
|
9
|
+
'kind': z.union([z.literal("file"), z.literal("url")]).readonly(),
|
|
10
|
+
'target': z.string().readonly(),
|
|
11
|
+
'title': z.string().readonly(),
|
|
12
|
+
}))
|
|
13
|
+
|
|
14
|
+
export const TYPERT_REMOTE = {
|
|
15
|
+
package: '@qilin-agent/sidebar-opens',
|
|
16
|
+
descriptors: [
|
|
17
|
+
{
|
|
18
|
+
id: '@qilin-agent/sidebar-opens#sidebarOpens/watch',
|
|
19
|
+
service: 'sidebarOpens',
|
|
20
|
+
namespace: 'sidebarOpens',
|
|
21
|
+
method: 'watch',
|
|
22
|
+
mode: 'stream',
|
|
23
|
+
invocation: { kind: 'direct' },
|
|
24
|
+
parameters: [
|
|
25
|
+
{
|
|
26
|
+
name: 'sessionId',
|
|
27
|
+
wire: 'sessionId',
|
|
28
|
+
source: 'json',
|
|
29
|
+
codec: {
|
|
30
|
+
mode: 'strict',
|
|
31
|
+
typeSymbol: '@qilin-agent/session/types#SessionId',
|
|
32
|
+
create: _qilin_agent_sidebar_opens_sidebarOpens_watch_parameter_0$schema,
|
|
33
|
+
},
|
|
34
|
+
},
|
|
35
|
+
],
|
|
36
|
+
cancellation: { parameter: 'signal' },
|
|
37
|
+
result: {
|
|
38
|
+
mode: 'strict',
|
|
39
|
+
typeSymbol: '@qilin-agent/sidebar-opens/types#SidebarOpenRequest',
|
|
40
|
+
create: _qilin_agent_sidebar_opens_sidebarOpens_watch_result$schema,
|
|
41
|
+
},
|
|
42
|
+
sourceLocation: {"file":"packages/host/sidebar-opens/src/index.ts","line":143,"column":10},
|
|
43
|
+
},
|
|
44
|
+
],
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export default TYPERT_REMOTE
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import type { Context } from '@qilin-agent/kylin';
|
|
2
|
+
import z from '@qilin-agent/schemastery';
|
|
3
|
+
import type { SessionId } from '@qilin-agent/session';
|
|
4
|
+
import { TypertRemoteService } from '@qilin-agent/typert-protocol';
|
|
5
|
+
import type { SidebarOpenRequest } from './types.ts';
|
|
6
|
+
export type * from './types.ts';
|
|
7
|
+
/** Stable Loader identity. */
|
|
8
|
+
export declare const name = "sidebar-opens";
|
|
9
|
+
/** Services the tool verifies its target through. */
|
|
10
|
+
export declare const inject: string[];
|
|
11
|
+
declare module '@qilin-agent/kylin' {
|
|
12
|
+
interface Context {
|
|
13
|
+
/** Pending and live sidebar opens, one bounded queue per Session. */
|
|
14
|
+
sidebarOpens: SidebarOpens;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
/** Deployment limits for the pending queue. */
|
|
18
|
+
export interface Config {
|
|
19
|
+
/** Open requests a Session may queue while no Sidebar view is attached. */
|
|
20
|
+
readonly maxQueued: number;
|
|
21
|
+
}
|
|
22
|
+
/** Schemastery validation for {@link Config}. */
|
|
23
|
+
export declare const Config: z<Config>;
|
|
24
|
+
/**
|
|
25
|
+
* Per-Session open queues with their attached watchers.
|
|
26
|
+
*
|
|
27
|
+
* One request has one destination: with a watcher attached it is pushed there
|
|
28
|
+
* and forgotten, and with none it waits in the Session's queue until a view
|
|
29
|
+
* attaches. The queue is bounded because a Session nobody is watching must not
|
|
30
|
+
* accumulate requests without end.
|
|
31
|
+
*/
|
|
32
|
+
export declare class SidebarOpens extends TypertRemoteService {
|
|
33
|
+
private readonly maxQueued;
|
|
34
|
+
private readonly pending;
|
|
35
|
+
private readonly watchers;
|
|
36
|
+
/**
|
|
37
|
+
* @param ctx - owning Host context.
|
|
38
|
+
* @param maxQueued - requests one Session may queue while no view is attached.
|
|
39
|
+
*/
|
|
40
|
+
constructor(ctx: Context, maxQueued: number);
|
|
41
|
+
/**
|
|
42
|
+
* Deliver one request to the Session's view, or queue it for the next one.
|
|
43
|
+
* @param sessionId - the Session whose Sidebar the request targets.
|
|
44
|
+
* @param request - the resolved request.
|
|
45
|
+
* @returns whether an attached view consumed it now.
|
|
46
|
+
*/
|
|
47
|
+
enqueue(sessionId: SessionId, request: SidebarOpenRequest): boolean;
|
|
48
|
+
/**
|
|
49
|
+
* Watch one Session's opens: what queued while nothing was attached, then
|
|
50
|
+
* every request as it arrives.
|
|
51
|
+
* @param sessionId - the Session whose Sidebar is watching.
|
|
52
|
+
* @param signal - physical Remote stream cancellation.
|
|
53
|
+
* @returns the queued requests followed by the live ones.
|
|
54
|
+
*/
|
|
55
|
+
watch(sessionId: SessionId, signal: AbortSignal): AsyncIterable<SidebarOpenRequest>;
|
|
56
|
+
/**
|
|
57
|
+
* Drop every queue, for a Host that is going away.
|
|
58
|
+
*/
|
|
59
|
+
dispose(): void;
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Register the `sidebar_open` tool and its delivery service.
|
|
63
|
+
* @param ctx - Host context with the tool registry and the filesystem.
|
|
64
|
+
* @param config - validated queue bound.
|
|
65
|
+
*/
|
|
66
|
+
export declare function apply(ctx: Context, config: Config): void;
|
|
67
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1,313 @@
|
|
|
1
|
+
var __runInitializers = (this && this.__runInitializers) || function (thisArg, initializers, value) {
|
|
2
|
+
var useValue = arguments.length > 2;
|
|
3
|
+
for (var i = 0; i < initializers.length; i++) {
|
|
4
|
+
value = useValue ? initializers[i].call(thisArg, value) : initializers[i].call(thisArg);
|
|
5
|
+
}
|
|
6
|
+
return useValue ? value : void 0;
|
|
7
|
+
};
|
|
8
|
+
var __esDecorate = (this && this.__esDecorate) || function (ctor, descriptorIn, decorators, contextIn, initializers, extraInitializers) {
|
|
9
|
+
function accept(f) { if (f !== void 0 && typeof f !== "function") throw new TypeError("Function expected"); return f; }
|
|
10
|
+
var kind = contextIn.kind, key = kind === "getter" ? "get" : kind === "setter" ? "set" : "value";
|
|
11
|
+
var target = !descriptorIn && ctor ? contextIn["static"] ? ctor : ctor.prototype : null;
|
|
12
|
+
var descriptor = descriptorIn || (target ? Object.getOwnPropertyDescriptor(target, contextIn.name) : {});
|
|
13
|
+
var _, done = false;
|
|
14
|
+
for (var i = decorators.length - 1; i >= 0; i--) {
|
|
15
|
+
var context = {};
|
|
16
|
+
for (var p in contextIn) context[p] = p === "access" ? {} : contextIn[p];
|
|
17
|
+
for (var p in contextIn.access) context.access[p] = contextIn.access[p];
|
|
18
|
+
context.addInitializer = function (f) { if (done) throw new TypeError("Cannot add initializers after decoration has completed"); extraInitializers.push(accept(f || null)); };
|
|
19
|
+
var result = (0, decorators[i])(kind === "accessor" ? { get: descriptor.get, set: descriptor.set } : descriptor[key], context);
|
|
20
|
+
if (kind === "accessor") {
|
|
21
|
+
if (result === void 0) continue;
|
|
22
|
+
if (result === null || typeof result !== "object") throw new TypeError("Object expected");
|
|
23
|
+
if (_ = accept(result.get)) descriptor.get = _;
|
|
24
|
+
if (_ = accept(result.set)) descriptor.set = _;
|
|
25
|
+
if (_ = accept(result.init)) initializers.unshift(_);
|
|
26
|
+
}
|
|
27
|
+
else if (_ = accept(result)) {
|
|
28
|
+
if (kind === "field") initializers.unshift(_);
|
|
29
|
+
else descriptor[key] = _;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
if (target) Object.defineProperty(target, contextIn.name, descriptor);
|
|
33
|
+
done = true;
|
|
34
|
+
};
|
|
35
|
+
/**
|
|
36
|
+
* `sidebar_open`: the model-facing request to show a file or an http(s) page in
|
|
37
|
+
* the Sidebar of its own Session.
|
|
38
|
+
*
|
|
39
|
+
* Delivery is a Host-to-browser push over a Remote stream (`watch`): a request
|
|
40
|
+
* is consumed on send while a view for that Session is attached, and otherwise
|
|
41
|
+
* waits in a bounded per-Session queue that a later attachment replays. A
|
|
42
|
+
* request is never durable: replaying a historical open on every page load
|
|
43
|
+
* would reopen a tab the user closed, which is why this is a stream and not a
|
|
44
|
+
* Session event. The tool call and its result stay in the Session log like any
|
|
45
|
+
* other tool, so what the model asked for is still reconstructable.
|
|
46
|
+
*/
|
|
47
|
+
import { randomUUID } from 'node:crypto';
|
|
48
|
+
import { basename } from 'node:path';
|
|
49
|
+
import z from '@qilin-agent/schemastery';
|
|
50
|
+
import { defineTool } from '@qilin-agent/tools';
|
|
51
|
+
import { Remote, TypertRemoteService } from '@qilin-agent/typert-protocol';
|
|
52
|
+
/** Stable Loader identity. */
|
|
53
|
+
export const name = 'sidebar-opens';
|
|
54
|
+
/** Services the tool verifies its target through. */
|
|
55
|
+
export const inject = ['tools', 'fs'];
|
|
56
|
+
/** Schemastery validation for {@link Config}. */
|
|
57
|
+
export const Config = z.object({
|
|
58
|
+
maxQueued: z.number().default(16),
|
|
59
|
+
});
|
|
60
|
+
/** One live watcher's inbox: pushed requests, and the wait a generator parks on. */
|
|
61
|
+
class Inbox {
|
|
62
|
+
items = [];
|
|
63
|
+
wake;
|
|
64
|
+
closed = false;
|
|
65
|
+
/**
|
|
66
|
+
* @param request - the next request for the watcher.
|
|
67
|
+
*/
|
|
68
|
+
push(request) {
|
|
69
|
+
this.items.push(request);
|
|
70
|
+
this.wake?.();
|
|
71
|
+
this.wake = undefined;
|
|
72
|
+
}
|
|
73
|
+
/** Stop delivering: the parked wait resolves empty and later pushes are dropped. */
|
|
74
|
+
close() {
|
|
75
|
+
this.closed = true;
|
|
76
|
+
this.wake?.();
|
|
77
|
+
this.wake = undefined;
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* The next request, the moment one arrives.
|
|
81
|
+
*
|
|
82
|
+
* A parked wait is released by {@link close}, which a watcher's own abort
|
|
83
|
+
* handler calls, so this adds no listener of its own: one per wait would
|
|
84
|
+
* accumulate on the transport's signal for the whole life of the watch.
|
|
85
|
+
* @returns the request, or undefined once the watcher is closed.
|
|
86
|
+
*/
|
|
87
|
+
async next() {
|
|
88
|
+
while (this.items.length === 0) {
|
|
89
|
+
if (this.closed)
|
|
90
|
+
return undefined;
|
|
91
|
+
await new Promise((resolve) => {
|
|
92
|
+
this.wake = resolve;
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
return this.items.shift();
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Per-Session open queues with their attached watchers.
|
|
100
|
+
*
|
|
101
|
+
* One request has one destination: with a watcher attached it is pushed there
|
|
102
|
+
* and forgotten, and with none it waits in the Session's queue until a view
|
|
103
|
+
* attaches. The queue is bounded because a Session nobody is watching must not
|
|
104
|
+
* accumulate requests without end.
|
|
105
|
+
*/
|
|
106
|
+
let SidebarOpens = (() => {
|
|
107
|
+
let _classSuper = TypertRemoteService;
|
|
108
|
+
let _instanceExtraInitializers = [];
|
|
109
|
+
let _watch_decorators;
|
|
110
|
+
return class SidebarOpens extends _classSuper {
|
|
111
|
+
static {
|
|
112
|
+
const _metadata = typeof Symbol === "function" && Symbol.metadata ? Object.create(_classSuper[Symbol.metadata] ?? null) : void 0;
|
|
113
|
+
_watch_decorators = [Remote({ mode: 'stream' })];
|
|
114
|
+
__esDecorate(this, null, _watch_decorators, { kind: "method", name: "watch", static: false, private: false, access: { has: obj => "watch" in obj, get: obj => obj.watch }, metadata: _metadata }, null, _instanceExtraInitializers);
|
|
115
|
+
if (_metadata) Object.defineProperty(this, Symbol.metadata, { enumerable: true, configurable: true, writable: true, value: _metadata });
|
|
116
|
+
}
|
|
117
|
+
maxQueued = __runInitializers(this, _instanceExtraInitializers);
|
|
118
|
+
pending = new Map();
|
|
119
|
+
watchers = new Map();
|
|
120
|
+
/**
|
|
121
|
+
* @param ctx - owning Host context.
|
|
122
|
+
* @param maxQueued - requests one Session may queue while no view is attached.
|
|
123
|
+
*/
|
|
124
|
+
constructor(ctx, maxQueued) {
|
|
125
|
+
super(ctx, 'sidebarOpens');
|
|
126
|
+
this.maxQueued = maxQueued;
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* Deliver one request to the Session's view, or queue it for the next one.
|
|
130
|
+
* @param sessionId - the Session whose Sidebar the request targets.
|
|
131
|
+
* @param request - the resolved request.
|
|
132
|
+
* @returns whether an attached view consumed it now.
|
|
133
|
+
*/
|
|
134
|
+
enqueue(sessionId, request) {
|
|
135
|
+
const watcher = this.watchers.get(sessionId);
|
|
136
|
+
if (watcher !== undefined) {
|
|
137
|
+
watcher.push(request);
|
|
138
|
+
return true;
|
|
139
|
+
}
|
|
140
|
+
const queue = this.pending.get(sessionId);
|
|
141
|
+
if (queue === undefined) {
|
|
142
|
+
this.pending.set(sessionId, [request]);
|
|
143
|
+
return false;
|
|
144
|
+
}
|
|
145
|
+
queue.push(request);
|
|
146
|
+
// The oldest request is the one a stalled viewer has already been told
|
|
147
|
+
// about least often; drop it rather than grow without bound.
|
|
148
|
+
if (queue.length > this.maxQueued)
|
|
149
|
+
queue.shift();
|
|
150
|
+
return false;
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* Watch one Session's opens: what queued while nothing was attached, then
|
|
154
|
+
* every request as it arrives.
|
|
155
|
+
* @param sessionId - the Session whose Sidebar is watching.
|
|
156
|
+
* @param signal - physical Remote stream cancellation.
|
|
157
|
+
* @returns the queued requests followed by the live ones.
|
|
158
|
+
*/
|
|
159
|
+
async *watch(sessionId, signal) {
|
|
160
|
+
const queued = this.pending.get(sessionId) ?? [];
|
|
161
|
+
this.pending.delete(sessionId);
|
|
162
|
+
const inbox = new Inbox();
|
|
163
|
+
// A second view for the same Session takes over: the newest watcher is the
|
|
164
|
+
// one the user is looking at.
|
|
165
|
+
this.watchers.get(sessionId)?.close();
|
|
166
|
+
this.watchers.set(sessionId, inbox);
|
|
167
|
+
const release = () => {
|
|
168
|
+
if (this.watchers.get(sessionId) === inbox)
|
|
169
|
+
this.watchers.delete(sessionId);
|
|
170
|
+
inbox.close();
|
|
171
|
+
};
|
|
172
|
+
signal.addEventListener('abort', release, { once: true });
|
|
173
|
+
try {
|
|
174
|
+
for (const request of queued) {
|
|
175
|
+
if (signal.aborted)
|
|
176
|
+
return;
|
|
177
|
+
yield request;
|
|
178
|
+
}
|
|
179
|
+
while (!signal.aborted) {
|
|
180
|
+
const request = await inbox.next();
|
|
181
|
+
if (request === undefined)
|
|
182
|
+
return;
|
|
183
|
+
yield request;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
finally {
|
|
187
|
+
signal.removeEventListener('abort', release);
|
|
188
|
+
release();
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
/**
|
|
192
|
+
* Drop every queue, for a Host that is going away.
|
|
193
|
+
*/
|
|
194
|
+
dispose() {
|
|
195
|
+
this.pending.clear();
|
|
196
|
+
for (const inbox of this.watchers.values())
|
|
197
|
+
inbox.close();
|
|
198
|
+
this.watchers.clear();
|
|
199
|
+
}
|
|
200
|
+
};
|
|
201
|
+
})();
|
|
202
|
+
export { SidebarOpens };
|
|
203
|
+
/** The exact http(s) URL a request may open. */
|
|
204
|
+
function pageUrl(value) {
|
|
205
|
+
let url;
|
|
206
|
+
try {
|
|
207
|
+
url = new URL(value);
|
|
208
|
+
}
|
|
209
|
+
catch {
|
|
210
|
+
return undefined;
|
|
211
|
+
}
|
|
212
|
+
return url.protocol === 'http:' || url.protocol === 'https:' ? url.toString() : undefined;
|
|
213
|
+
}
|
|
214
|
+
/**
|
|
215
|
+
* Register the `sidebar_open` tool and its delivery service.
|
|
216
|
+
* @param ctx - Host context with the tool registry and the filesystem.
|
|
217
|
+
* @param config - validated queue bound.
|
|
218
|
+
*/
|
|
219
|
+
export function apply(ctx, config) {
|
|
220
|
+
if (!Number.isSafeInteger(config.maxQueued) || config.maxQueued < 1) {
|
|
221
|
+
throw new Error('sidebar-opens requires a positive integer maxQueued');
|
|
222
|
+
}
|
|
223
|
+
const opens = new SidebarOpens(ctx, config.maxQueued);
|
|
224
|
+
ctx.effect(() => () => { opens.dispose(); }, 'sidebar-opens: queues');
|
|
225
|
+
ctx.tools.register(defineTool({
|
|
226
|
+
name: 'sidebar_open',
|
|
227
|
+
description: 'Open one file or one http(s) page in the Sidebar the user is viewing this Session in. '
|
|
228
|
+
+ 'Use it when the user asked to see something: a file you produced, a file worth reading beside the conversation, '
|
|
229
|
+
+ 'or a page you found. Pass exactly one of `path` (a file that already exists) or `url`. '
|
|
230
|
+
+ 'The file opens in the document preview and the page in the built-in browser; both appear beside the conversation '
|
|
231
|
+
+ 'rather than leaving the application.',
|
|
232
|
+
parameters: {
|
|
233
|
+
path: {
|
|
234
|
+
type: 'string',
|
|
235
|
+
description: 'Path of an existing regular file, relative to the Session working directory or absolute.',
|
|
236
|
+
},
|
|
237
|
+
url: {
|
|
238
|
+
type: 'string',
|
|
239
|
+
description: 'An http:// or https:// page to open in the built-in browser.',
|
|
240
|
+
},
|
|
241
|
+
},
|
|
242
|
+
output: {
|
|
243
|
+
schema: {
|
|
244
|
+
type: 'object', additionalProperties: false,
|
|
245
|
+
properties: {
|
|
246
|
+
kind: { type: 'string', required: true, enum: ['file', 'url'] },
|
|
247
|
+
target: { type: 'string', required: true },
|
|
248
|
+
title: { type: 'string', required: true },
|
|
249
|
+
delivered: { type: 'boolean', required: true },
|
|
250
|
+
},
|
|
251
|
+
},
|
|
252
|
+
render: (_args, value) => [{
|
|
253
|
+
type: 'text',
|
|
254
|
+
text: value.delivered
|
|
255
|
+
? `Opened ${value.target} in the sidebar.`
|
|
256
|
+
: `Queued ${value.target} to open in the sidebar as soon as that Session's sidebar is shown.`,
|
|
257
|
+
}],
|
|
258
|
+
},
|
|
259
|
+
async execute(args, exec) {
|
|
260
|
+
if (exec.agent === undefined)
|
|
261
|
+
throw new Error('sidebar_open requires an initiating agent');
|
|
262
|
+
const hasPath = args.path !== undefined && args.path.trim().length > 0;
|
|
263
|
+
const hasUrl = args.url !== undefined && args.url.trim().length > 0;
|
|
264
|
+
if (hasPath === hasUrl)
|
|
265
|
+
throw new Error('sidebar_open requires exactly one of path or url');
|
|
266
|
+
const session = exec.agent.session;
|
|
267
|
+
const request = hasUrl
|
|
268
|
+
? urlRequest(args.url)
|
|
269
|
+
: await fileRequest(ctx, exec.signal, session.header.cwd, args.path);
|
|
270
|
+
// The host-minted identity stays internal: the model is told what it
|
|
271
|
+
// asked for, not how the browser will dedupe it.
|
|
272
|
+
return {
|
|
273
|
+
kind: request.kind, target: request.target, title: request.title,
|
|
274
|
+
delivered: opens.enqueue(session.id, request),
|
|
275
|
+
};
|
|
276
|
+
},
|
|
277
|
+
}));
|
|
278
|
+
}
|
|
279
|
+
/**
|
|
280
|
+
* One page request from the model's URL.
|
|
281
|
+
* @param value - the URL as the model wrote it.
|
|
282
|
+
* @returns the request to deliver.
|
|
283
|
+
* @throws when the URL is malformed or is not http(s).
|
|
284
|
+
*/
|
|
285
|
+
function urlRequest(value) {
|
|
286
|
+
const target = pageUrl(value);
|
|
287
|
+
if (target === undefined)
|
|
288
|
+
throw new Error('sidebar_open only opens http:// and https:// URLs; pass a path for a local file');
|
|
289
|
+
return { id: randomUUID(), kind: 'url', target, title: new URL(target).host };
|
|
290
|
+
}
|
|
291
|
+
/**
|
|
292
|
+
* One file request, verified through the Session filesystem before it is made
|
|
293
|
+
* so the browser never opens a path that does not resolve.
|
|
294
|
+
* @param ctx - Host context carrying the filesystem.
|
|
295
|
+
* @param signal - tool cancellation.
|
|
296
|
+
* @param cwd - the Session working directory relative paths resolve against.
|
|
297
|
+
* @param value - the path as the model wrote it.
|
|
298
|
+
* @returns the request to deliver.
|
|
299
|
+
* @throws when the path does not name a regular file.
|
|
300
|
+
*/
|
|
301
|
+
async function fileRequest(ctx, signal, cwd, value) {
|
|
302
|
+
signal.throwIfAborted();
|
|
303
|
+
const target = await ctx.fs.resolve(value, { ...cwd === undefined ? {} : { cwd }, signal });
|
|
304
|
+
const info = await ctx.fs.stat(target, signal);
|
|
305
|
+
if (info === undefined)
|
|
306
|
+
throw new Error(`Cannot open ${value}: file not found. Check the path and retry.`);
|
|
307
|
+
if (info.type !== 'file')
|
|
308
|
+
throw new Error(`Cannot open ${value}: not a regular file`);
|
|
309
|
+
signal.throwIfAborted();
|
|
310
|
+
const path = ctx.fs.processPath(target);
|
|
311
|
+
return { id: randomUUID(), kind: 'file', target: path, title: basename(path) };
|
|
312
|
+
}
|
|
313
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/** What the model asked the sidebar to show, and how one request reaches the browser. */
|
|
2
|
+
/** One open request, as the tool resolved it. */
|
|
3
|
+
export interface SidebarOpenRequest {
|
|
4
|
+
/** Host-minted identity, so a client can dedupe a replay it already applied. */
|
|
5
|
+
readonly id: string;
|
|
6
|
+
/** What the target is. */
|
|
7
|
+
readonly kind: 'file' | 'url';
|
|
8
|
+
/**
|
|
9
|
+
* The absolute Host path of the file, or the normalized http(s) URL of the
|
|
10
|
+
* page. A file path is verified to be a regular file before the request is
|
|
11
|
+
* made; a URL is verified to be http(s).
|
|
12
|
+
*/
|
|
13
|
+
readonly target: string;
|
|
14
|
+
/** The tab title to use: the file's basename, or the page's host. */
|
|
15
|
+
readonly title: string;
|
|
16
|
+
}
|
|
17
|
+
//# sourceMappingURL=types.d.ts.map
|
package/package.json
CHANGED
|
@@ -1,6 +1,74 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@qilin-agent/sidebar-opens",
|
|
3
|
-
"
|
|
4
|
-
"
|
|
5
|
-
"
|
|
3
|
+
"description": "Model-facing requests to open a file or an http(s) page in the Session's Sidebar",
|
|
4
|
+
"version": "3.1.4",
|
|
5
|
+
"publishConfig": {
|
|
6
|
+
"access": "public"
|
|
7
|
+
},
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "git+https://github.com/kkutysllb/QiLin.git",
|
|
11
|
+
"directory": "packages/host/sidebar-opens"
|
|
12
|
+
},
|
|
13
|
+
"engines": {
|
|
14
|
+
"node": "^22.19.0 || >=24.0.0"
|
|
15
|
+
},
|
|
16
|
+
"type": "module",
|
|
17
|
+
"main": "lib/index.js",
|
|
18
|
+
"types": "lib/types/index.d.ts",
|
|
19
|
+
"exports": {
|
|
20
|
+
".": {
|
|
21
|
+
"types": "./lib/types/index.d.ts",
|
|
22
|
+
"default": "./lib/index.js"
|
|
23
|
+
},
|
|
24
|
+
"./types": {
|
|
25
|
+
"types": "./lib/types/types.d.ts",
|
|
26
|
+
"default": "./lib/types/types.js"
|
|
27
|
+
},
|
|
28
|
+
"./typert": {
|
|
29
|
+
"types": "./lib/typert.host.d.ts",
|
|
30
|
+
"default": "./lib/typert.host.js"
|
|
31
|
+
},
|
|
32
|
+
"./remote": {
|
|
33
|
+
"types": "./lib/typert.remote-client.d.ts",
|
|
34
|
+
"default": "./lib/typert.remote-client.js"
|
|
35
|
+
},
|
|
36
|
+
"./src/*": "./src/*",
|
|
37
|
+
"./package.json": "./package.json"
|
|
38
|
+
},
|
|
39
|
+
"license": "MIT",
|
|
40
|
+
"files": [
|
|
41
|
+
"lib/index.js",
|
|
42
|
+
"lib/types/**/*.js",
|
|
43
|
+
"lib/types/**/*.d.ts",
|
|
44
|
+
"lib/typert.host.js",
|
|
45
|
+
"lib/typert.host.d.ts",
|
|
46
|
+
"lib/typert.remote-client.js",
|
|
47
|
+
"lib/typert.remote-client.d.ts"
|
|
48
|
+
],
|
|
49
|
+
"dependencies": {
|
|
50
|
+
"zod": "^4.4.3",
|
|
51
|
+
"@qilin-agent/schemastery": "~3.18.3"
|
|
52
|
+
},
|
|
53
|
+
"peerDependencies": {
|
|
54
|
+
"@qilin-agent/agent": "3.1.4",
|
|
55
|
+
"@qilin-agent/fs": "3.1.4",
|
|
56
|
+
"@qilin-agent/kylin": "~4.0.3",
|
|
57
|
+
"@qilin-agent/session": "3.1.4",
|
|
58
|
+
"@qilin-agent/typert-protocol": "3.1.4",
|
|
59
|
+
"@qilin-agent/tools": "3.1.4"
|
|
60
|
+
},
|
|
61
|
+
"devDependencies": {
|
|
62
|
+
"@qilin-agent/agent": "3.1.4",
|
|
63
|
+
"@qilin-agent/agent-loop-testkit": "3.1.4",
|
|
64
|
+
"@qilin-agent/kylin": "~4.0.3",
|
|
65
|
+
"@qilin-agent/fs": "3.1.4",
|
|
66
|
+
"@qilin-agent/fs-local": "3.1.4",
|
|
67
|
+
"@qilin-agent/llm": "3.1.4",
|
|
68
|
+
"@qilin-agent/scope": "3.1.4",
|
|
69
|
+
"@qilin-agent/session": "3.1.4",
|
|
70
|
+
"@qilin-agent/typert-protocol": "3.1.4",
|
|
71
|
+
"@qilin-agent/tools": "3.1.4",
|
|
72
|
+
"@qilin-agent/system-prompt": "3.1.4"
|
|
73
|
+
}
|
|
6
74
|
}
|