@wowyuarm/dsh-agent-team 0.1.0 → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +14 -2
- package/README.zh.md +10 -2
- package/package.json +2 -2
- package/packages/agent-team/lib/attachments.js +157 -0
- package/packages/agent-team/lib/index.js +274 -5
- package/packages/agent-team/lib/ledger.js +65 -13
- package/packages/agent-team/lib/recovery.js +87 -0
- package/packages/agent-team/lib/spec.js +17 -1
- package/packages/agent-team/lib/typert.host.js +321 -21
- package/packages/agent-team/lib/typert.remote-client.d.ts +9 -1
- package/packages/agent-team/lib/typert.remote-client.d.ts.map +1 -1
- package/packages/agent-team/lib/typert.remote-client.js +249 -17
- package/packages/agent-team/lib/types/attachments.d.ts +69 -0
- package/packages/agent-team/lib/types/attachments.d.ts.map +1 -0
- package/packages/agent-team/lib/types/index.d.ts +66 -1
- package/packages/agent-team/lib/types/index.d.ts.map +1 -1
- package/packages/agent-team/lib/types/ledger.d.ts +17 -1
- package/packages/agent-team/lib/types/ledger.d.ts.map +1 -1
- package/packages/agent-team/lib/types/recovery.d.ts +42 -0
- package/packages/agent-team/lib/types/recovery.d.ts.map +1 -0
- package/packages/agent-team/lib/types/spec.d.ts.map +1 -1
- package/packages/agent-team/lib/types/types.d.ts +85 -2
- package/packages/agent-team/lib/types/types.d.ts.map +1 -1
- package/packages/client-agent-team/README.md +2 -2
- package/packages/client-agent-team/README.zh.md +2 -2
- package/packages/client-agent-team/lib/client.js +1176 -281
- package/packages/client-agent-team/lib/client.js.map +1 -1
- package/packages/client-agent-team/lib/types/client/TeamAgentsPanel.d.ts +6 -2
- package/packages/client-agent-team/lib/types/client/TeamAgentsPanel.d.ts.map +1 -1
- package/packages/client-agent-team/lib/types/client/TeamAgentsPanel.js +104 -58
- package/packages/client-agent-team/lib/types/client/TeamChannelPage.d.ts +5 -1
- package/packages/client-agent-team/lib/types/client/TeamChannelPage.d.ts.map +1 -1
- package/packages/client-agent-team/lib/types/client/TeamChannelPage.js +44 -19
- package/packages/client-agent-team/lib/types/client/TeamChannelsPanel.d.ts.map +1 -1
- package/packages/client-agent-team/lib/types/client/TeamChannelsPanel.js +23 -18
- package/packages/client-agent-team/lib/types/client/TeamComposer.d.ts +4 -1
- package/packages/client-agent-team/lib/types/client/TeamComposer.d.ts.map +1 -1
- package/packages/client-agent-team/lib/types/client/TeamComposer.js +40 -4
- package/packages/client-agent-team/lib/types/client/TeamConversation.d.ts +1 -1
- package/packages/client-agent-team/lib/types/client/TeamConversation.d.ts.map +1 -1
- package/packages/client-agent-team/lib/types/client/TeamConversation.js +3 -3
- package/packages/client-agent-team/lib/types/client/TeamMessage.d.ts +7 -2
- package/packages/client-agent-team/lib/types/client/TeamMessage.d.ts.map +1 -1
- package/packages/client-agent-team/lib/types/client/TeamMessage.js +34 -8
- package/packages/client-agent-team/lib/types/client/TeamRunDivider.d.ts +10 -0
- package/packages/client-agent-team/lib/types/client/TeamRunDivider.d.ts.map +1 -0
- package/packages/client-agent-team/lib/types/client/TeamRunDivider.js +12 -0
- package/packages/client-agent-team/lib/types/client/TeamThreadPage.d.ts +3 -0
- package/packages/client-agent-team/lib/types/client/TeamThreadPage.d.ts.map +1 -1
- package/packages/client-agent-team/lib/types/client/TeamThreadPage.js +18 -11
- package/packages/client-agent-team/lib/types/client/TeamWorkspaceBrowser.d.ts +1 -1
- package/packages/client-agent-team/lib/types/client/TeamWorkspaceBrowser.d.ts.map +1 -1
- package/packages/client-agent-team/lib/types/client/TeamWorkspaceBrowser.js +6 -5
- package/packages/client-agent-team/lib/types/client/attachment-preview.d.ts +22 -0
- package/packages/client-agent-team/lib/types/client/attachment-preview.d.ts.map +1 -0
- package/packages/client-agent-team/lib/types/client/attachment-preview.js +35 -0
- package/packages/client-agent-team/lib/types/client/drafts.d.ts +41 -0
- package/packages/client-agent-team/lib/types/client/drafts.d.ts.map +1 -0
- package/packages/client-agent-team/lib/types/client/drafts.js +144 -0
- package/packages/client-agent-team/lib/types/client/index.d.ts +2 -0
- package/packages/client-agent-team/lib/types/client/index.d.ts.map +1 -1
- package/packages/client-agent-team/lib/types/client/index.js +50 -10
- package/packages/client-agent-team/lib/types/client/locales.d.ts +28 -0
- package/packages/client-agent-team/lib/types/client/locales.d.ts.map +1 -1
- package/packages/client-agent-team/lib/types/client/locales.js +28 -0
- package/packages/client-agent-team/lib/types/client/multi-menu-field.d.ts +33 -0
- package/packages/client-agent-team/lib/types/client/multi-menu-field.d.ts.map +1 -0
- package/packages/client-agent-team/lib/types/client/multi-menu-field.js +30 -0
- package/packages/client-agent-team/lib/types/client/navigation.d.ts +16 -1
- package/packages/client-agent-team/lib/types/client/navigation.d.ts.map +1 -1
- package/packages/client-agent-team/lib/types/client/navigation.js +45 -8
- package/packages/client-agent-team/lib/types/client/slots.d.ts +18 -2
- package/packages/client-agent-team/lib/types/client/slots.d.ts.map +1 -1
- package/packages/client-agent-team/lib/types/client/team-formatters.d.ts +2 -0
- package/packages/client-agent-team/lib/types/client/team-formatters.d.ts.map +1 -1
- package/packages/client-agent-team/lib/types/client/team-formatters.js +4 -0
- package/packages/client-agent-team/lib/types/client/team-separators.d.ts +8 -0
- package/packages/client-agent-team/lib/types/client/team-separators.d.ts.map +1 -1
- package/packages/client-agent-team/lib/types/client/team-separators.js +14 -0
- package/packages/tool-agent-team/lib/index.js +6 -2
- package/packages/tool-agent-team/lib/types/index.d.ts.map +1 -1
package/README.md
CHANGED
|
@@ -2,9 +2,13 @@
|
|
|
2
2
|
|
|
3
3
|
[English](README.md) | [简体中文](README.zh.md)
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
[](https://www.npmjs.com/package/@wowyuarm/dsh-agent-team)
|
|
6
|
+
[](LICENSE)
|
|
7
|
+
[](https://github.com/wowyuarm/dsh-agent-team/releases)
|
|
6
8
|
|
|
7
|
-
|
|
9
|
+
**dsh-agent-team** helps a human manage tasks in an orderly way and use agents as true collaborators: agents are persistent identities for their sessions; Workspaces organize agents and sessions per project; Channels carry responsibilities; Task Threads chain session agents into one line of progress.
|
|
10
|
+
|
|
11
|
+
An opt-in plugin for [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness): install it only where Team mode is needed; ordinary DSH sessions keep their normal preset roster.
|
|
8
12
|
|
|
9
13
|
## Preview
|
|
10
14
|
|
|
@@ -53,6 +57,14 @@ Team mode
|
|
|
53
57
|
|
|
54
58
|
Create an Agent only in a trusted Workspace. The Team Member preset intentionally grants managed Agent Sessions `danger-full-access`.
|
|
55
59
|
|
|
60
|
+
## Uninstall
|
|
61
|
+
|
|
62
|
+
Remove the bundle from the profile; this also removes its composed layers:
|
|
63
|
+
|
|
64
|
+
```sh
|
|
65
|
+
dsh plugin --profile web remove @wowyuarm/dsh-agent-team
|
|
66
|
+
```
|
|
67
|
+
|
|
56
68
|
## What it adds
|
|
57
69
|
|
|
58
70
|
- A durable single-host Team with Channels, Messages, Tasks, Threads, Claims, and Agent membership.
|
package/README.zh.md
CHANGED
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
[English](README.md) | 简体中文
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
**dsh-agent-team** 帮助 human 有序管理任务、把 agents 用成真正的协作者:Agent 是 session 的持久身份;Workspace 按项目组织 agents 与 sessions;Channel 承载职责分派;Task Thread 把多个 session agent 串成一条推进线。
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
一个为 [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness) 提供的按需启用插件:只在需要 Team mode 的 profile 安装,普通 DSH Session 保持原有 preset roster。
|
|
8
8
|
|
|
9
9
|
## 预览
|
|
10
10
|
|
|
@@ -53,6 +53,14 @@ Team mode
|
|
|
53
53
|
|
|
54
54
|
只在可信 Workspace 中创建 Agent。Team Member preset 会给被管理的 Agent Session 授予 `danger-full-access`。
|
|
55
55
|
|
|
56
|
+
## 卸载
|
|
57
|
+
|
|
58
|
+
从 profile 移除 bundle,同时会移除它组合进来的层:
|
|
59
|
+
|
|
60
|
+
```sh
|
|
61
|
+
dsh plugin --profile web remove @wowyuarm/dsh-agent-team
|
|
62
|
+
```
|
|
63
|
+
|
|
56
64
|
## 提供的能力
|
|
57
65
|
|
|
58
66
|
- 持久化的单 Host Team,包含 Channel、Message、Task、Thread、Claim 和 Agent membership。
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@wowyuarm/dsh-agent-team",
|
|
3
|
-
"version": "0.1.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "0.1.1",
|
|
4
|
+
"description": "Help humans organize tasks and let agents collaborate - a Team plugin for DeepSeek Harness",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
7
7
|
"url": "https://github.com/wowyuarm/dsh-agent-team.git"
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
import { mkdir, readFile, readdir, rm, stat, writeFile } from 'node:fs/promises';
|
|
2
|
+
import { basename, extname, isAbsolute, join } from 'node:path';
|
|
3
|
+
import { randomUUID } from 'node:crypto';
|
|
4
|
+
import { dshHomePath } from '@deepseek-ai/dsh-home-paths';
|
|
5
|
+
/**
|
|
6
|
+
* Composer attachments are a cache, not an archive: bytes live only so Member
|
|
7
|
+
* agents can read them within the consumption window, while the ledger keeps
|
|
8
|
+
* the metadata forever. Everything here derives from the on-disk layout
|
|
9
|
+
* `$DSH_HOME/agent-team/attachments/v1/<attachmentId>/` holding the payload
|
|
10
|
+
* file (original sanitized name) plus a `meta.json` sidecar.
|
|
11
|
+
*/
|
|
12
|
+
export const ATTACHMENT_MAX_BYTES = 10 * 1024 * 1024;
|
|
13
|
+
/** Referenced uploads survive this long after upload for member consumption. */
|
|
14
|
+
export const ATTACHMENT_REFERENCED_TTL_MS = 72 * 60 * 60 * 1000;
|
|
15
|
+
/** Unreferenced uploads (uploaded but never sent) are cleaned much sooner. */
|
|
16
|
+
export const ATTACHMENT_ORPHAN_TTL_MS = 24 * 60 * 60 * 1000;
|
|
17
|
+
export function attachmentsRoot() {
|
|
18
|
+
return dshHomePath('agent-team', 'attachments', 'v1');
|
|
19
|
+
}
|
|
20
|
+
export function newAttachmentId() {
|
|
21
|
+
return randomUUID();
|
|
22
|
+
}
|
|
23
|
+
/** Strip path separators, control characters, and leading dots from one client-supplied name. */
|
|
24
|
+
export function sanitizeFileName(raw) {
|
|
25
|
+
const cleaned = raw.replaceAll(/[\\/\u0000-\u001f\u007f]/g, '').replaceAll(/^\.+/g, '').trim();
|
|
26
|
+
return cleaned === '' ? 'attachment' : cleaned.slice(0, 180);
|
|
27
|
+
}
|
|
28
|
+
/** Extension-derived media types for agent-supplied files; unknown types stay generic. */
|
|
29
|
+
const PATH_MEDIA_TYPES = {
|
|
30
|
+
'.png': 'image/png',
|
|
31
|
+
'.jpg': 'image/jpeg',
|
|
32
|
+
'.jpeg': 'image/jpeg',
|
|
33
|
+
'.gif': 'image/gif',
|
|
34
|
+
'.webp': 'image/webp',
|
|
35
|
+
'.bmp': 'image/bmp',
|
|
36
|
+
'.svg': 'image/svg+xml',
|
|
37
|
+
'.pdf': 'application/pdf',
|
|
38
|
+
'.txt': 'text/plain',
|
|
39
|
+
'.md': 'text/markdown',
|
|
40
|
+
'.json': 'application/json',
|
|
41
|
+
};
|
|
42
|
+
/** Best-effort media type from one path's extension, so images render as thumbnails. */
|
|
43
|
+
export function mediaTypeForPath(raw) {
|
|
44
|
+
return PATH_MEDIA_TYPES[extname(raw).toLowerCase()] ?? 'application/octet-stream';
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Validate one agent-supplied attachment path before any cache write happens,
|
|
48
|
+
* so a rejection anywhere leaves the upload cache untouched.
|
|
49
|
+
*/
|
|
50
|
+
export async function validatePathAttachment(raw) {
|
|
51
|
+
if (!isAbsolute(raw))
|
|
52
|
+
throw new Error(`attachment path '${raw}' must be absolute`);
|
|
53
|
+
const info = await stat(raw).catch(() => undefined);
|
|
54
|
+
if (info === undefined)
|
|
55
|
+
throw new Error(`attachment path '${raw}' does not exist`);
|
|
56
|
+
if (!info.isFile())
|
|
57
|
+
throw new Error(`attachment path '${raw}' is not a regular file`);
|
|
58
|
+
if (info.size === 0)
|
|
59
|
+
throw new Error(`attachment '${basename(raw)}' must not be empty`);
|
|
60
|
+
if (info.size > ATTACHMENT_MAX_BYTES)
|
|
61
|
+
throw new Error(`attachment '${basename(raw)}' exceeds the ${ATTACHMENT_MAX_BYTES} byte limit`);
|
|
62
|
+
}
|
|
63
|
+
/** Copy one validated file into the cache as a fresh immutable entry. */
|
|
64
|
+
export async function copyPathAttachment(root, raw) {
|
|
65
|
+
const bytes = await readFile(raw);
|
|
66
|
+
const stored = await writeAttachment(root, newAttachmentId(), basename(raw), mediaTypeForPath(raw), bytes);
|
|
67
|
+
return { attachmentId: stored.attachmentId, name: stored.name, byteSize: stored.byteSize, mediaType: stored.mediaType };
|
|
68
|
+
}
|
|
69
|
+
function attachmentDir(root, attachmentId) {
|
|
70
|
+
return join(root, attachmentId);
|
|
71
|
+
}
|
|
72
|
+
/** Write one upload as an immutable payload plus its metadata sidecar. */
|
|
73
|
+
export async function writeAttachment(root, attachmentId, rawName, mediaType, bytes) {
|
|
74
|
+
const name = sanitizeFileName(rawName);
|
|
75
|
+
const dir = attachmentDir(root, attachmentId);
|
|
76
|
+
await mkdir(dir, { recursive: true });
|
|
77
|
+
await writeFile(join(dir, name), bytes);
|
|
78
|
+
const meta = { name, mediaType, uploadedAt: new Date().toISOString() };
|
|
79
|
+
await writeFile(join(dir, 'meta.json'), JSON.stringify(meta), 'utf8');
|
|
80
|
+
return { attachmentId, path: join(dir, name), name, byteSize: bytes.byteLength, mediaType };
|
|
81
|
+
}
|
|
82
|
+
/** Read one attachment back; `undefined` when the cache entry is gone (GC'd). */
|
|
83
|
+
export async function readAttachment(root, attachmentId) {
|
|
84
|
+
const dir = attachmentDir(root, attachmentId);
|
|
85
|
+
let metaRaw;
|
|
86
|
+
try {
|
|
87
|
+
metaRaw = await readFile(join(dir, 'meta.json'));
|
|
88
|
+
}
|
|
89
|
+
catch {
|
|
90
|
+
return undefined;
|
|
91
|
+
}
|
|
92
|
+
const meta = JSON.parse(metaRaw.toString('utf8'));
|
|
93
|
+
const entries = await readdir(dir);
|
|
94
|
+
const payload = entries.filter(entry => entry !== 'meta.json')[0];
|
|
95
|
+
if (payload === undefined)
|
|
96
|
+
return undefined;
|
|
97
|
+
const bytes = await readFile(join(dir, payload));
|
|
98
|
+
return { name: meta.name, mediaType: meta.mediaType, byteSize: bytes.byteLength, uploadedAt: meta.uploadedAt, bytes };
|
|
99
|
+
}
|
|
100
|
+
/** Whether an upload with this id still has bytes on disk. */
|
|
101
|
+
export async function attachmentExists(root, attachmentId) {
|
|
102
|
+
try {
|
|
103
|
+
await stat(attachmentDir(root, attachmentId));
|
|
104
|
+
return true;
|
|
105
|
+
}
|
|
106
|
+
catch {
|
|
107
|
+
return false;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
/** List every cache entry with its upload instant for the GC sweep. */
|
|
111
|
+
export async function scanAttachmentCache(root) {
|
|
112
|
+
let ids;
|
|
113
|
+
try {
|
|
114
|
+
ids = await readdir(root);
|
|
115
|
+
}
|
|
116
|
+
catch {
|
|
117
|
+
return [];
|
|
118
|
+
}
|
|
119
|
+
const entries = [];
|
|
120
|
+
for (const id of ids) {
|
|
121
|
+
try {
|
|
122
|
+
const metaRaw = await readFile(join(root, id, 'meta.json'));
|
|
123
|
+
const meta = JSON.parse(metaRaw.toString('utf8'));
|
|
124
|
+
entries.push({ attachmentId: id, uploadedAt: Date.parse(meta.uploadedAt) });
|
|
125
|
+
}
|
|
126
|
+
catch {
|
|
127
|
+
// A half-written or foreign directory is not ours to judge; skip it.
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
return entries;
|
|
131
|
+
}
|
|
132
|
+
/** Remove one cache entry's bytes; missing entries already satisfy the sweep. */
|
|
133
|
+
export async function removeAttachment(root, attachmentId) {
|
|
134
|
+
await rm(attachmentDir(root, attachmentId), { recursive: true, force: true });
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* One GC pass: drop uploads older than the orphan TTL, and referenced ones
|
|
138
|
+
* older than the consumption-window TTL. Returns the ids removed so the
|
|
139
|
+
* service can log them.
|
|
140
|
+
*/
|
|
141
|
+
export async function sweepAttachmentCache(root, referenced, now) {
|
|
142
|
+
const removed = [];
|
|
143
|
+
for (const entry of await scanAttachmentCache(root)) {
|
|
144
|
+
const age = now - entry.uploadedAt;
|
|
145
|
+
const ttl = referenced.has(entry.attachmentId) ? ATTACHMENT_REFERENCED_TTL_MS : ATTACHMENT_ORPHAN_TTL_MS;
|
|
146
|
+
if (age > ttl) {
|
|
147
|
+
await removeAttachment(root, entry.attachmentId);
|
|
148
|
+
removed.push(entry.attachmentId);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
return removed;
|
|
152
|
+
}
|
|
153
|
+
/** Accept only well-formed `type/subtype` media types; anything else falls back to the generic binary type. */
|
|
154
|
+
export function sanitizeMediaType(raw) {
|
|
155
|
+
const candidate = (raw ?? '').trim().toLowerCase();
|
|
156
|
+
return /^[a-z0-9][-.\w]*\/[-.\w]+$/.test(candidate) ? candidate : 'application/octet-stream';
|
|
157
|
+
}
|
|
@@ -50,7 +50,9 @@ import { scopeOf } from '@deepseek-ai/dsh-scope';
|
|
|
50
50
|
import { SessionId } from '@deepseek-ai/dsh-session';
|
|
51
51
|
import { effectiveSandboxMode, setSandboxMode } from '@deepseek-ai/dsh-sandbox-policy';
|
|
52
52
|
import { Remote, TypertRemoteService } from '@deepseek-ai/dsh-typert-protocol';
|
|
53
|
+
import { ATTACHMENT_MAX_BYTES, attachmentsRoot, copyPathAttachment, newAttachmentId, readAttachment, sanitizeMediaType, sweepAttachmentCache, validatePathAttachment, writeAttachment } from "./attachments.js";
|
|
53
54
|
import { AGENT_TEAM_HUMAN_MEMBER_ID, AgentTeamLedger, agentTeamHumanActor } from "./ledger.js";
|
|
55
|
+
import { classifyRecoverableError, RecoveryCoordinator, RECOVERY_MAX_ATTEMPTS } from "./recovery.js";
|
|
54
56
|
import { agentTeamDomainSpec } from "./spec.js";
|
|
55
57
|
export { agentTeamDomainSpec, agentTeamOperationSchema } from "./spec.js";
|
|
56
58
|
export { AGENT_TEAM_HUMAN_MEMBER_ID, AGENT_TEAM_INITIALIZE_REQUEST_ID } from "./ledger.js";
|
|
@@ -86,11 +88,15 @@ let AgentTeam = (() => {
|
|
|
86
88
|
let _createChannel_decorators;
|
|
87
89
|
let _updateChannel_decorators;
|
|
88
90
|
let _addMember_decorators;
|
|
91
|
+
let _recoverMember_decorators;
|
|
92
|
+
let _restartMember_decorators;
|
|
89
93
|
let _updateMember_decorators;
|
|
90
94
|
let _changeTask_decorators;
|
|
91
95
|
let _joinChannel_decorators;
|
|
92
96
|
let _removeChannelMember_decorators;
|
|
93
97
|
let _sendMessage_decorators;
|
|
98
|
+
let _putAttachment_decorators;
|
|
99
|
+
let _getAttachment_decorators;
|
|
94
100
|
let _reply_decorators;
|
|
95
101
|
let _changeAttention_decorators;
|
|
96
102
|
let _inbox_decorators;
|
|
@@ -106,11 +112,15 @@ let AgentTeam = (() => {
|
|
|
106
112
|
_createChannel_decorators = [Remote('createChannel')];
|
|
107
113
|
_updateChannel_decorators = [Remote('updateChannel')];
|
|
108
114
|
_addMember_decorators = [Remote('addMember')];
|
|
115
|
+
_recoverMember_decorators = [Remote('recoverMember')];
|
|
116
|
+
_restartMember_decorators = [Remote('restartMember')];
|
|
109
117
|
_updateMember_decorators = [Remote('updateMember')];
|
|
110
118
|
_changeTask_decorators = [Remote('changeTask')];
|
|
111
119
|
_joinChannel_decorators = [Remote('joinChannel')];
|
|
112
120
|
_removeChannelMember_decorators = [Remote('removeChannelMember')];
|
|
113
121
|
_sendMessage_decorators = [Remote('sendMessage')];
|
|
122
|
+
_putAttachment_decorators = [Remote('putAttachment')];
|
|
123
|
+
_getAttachment_decorators = [Remote('getAttachment')];
|
|
114
124
|
_reply_decorators = [Remote('reply')];
|
|
115
125
|
_changeAttention_decorators = [Remote('changeAttention')];
|
|
116
126
|
_inbox_decorators = [Remote('inbox')];
|
|
@@ -123,11 +133,15 @@ let AgentTeam = (() => {
|
|
|
123
133
|
__esDecorate(this, null, _createChannel_decorators, { kind: "method", name: "createChannel", static: false, private: false, access: { has: obj => "createChannel" in obj, get: obj => obj.createChannel }, metadata: _metadata }, null, _instanceExtraInitializers);
|
|
124
134
|
__esDecorate(this, null, _updateChannel_decorators, { kind: "method", name: "updateChannel", static: false, private: false, access: { has: obj => "updateChannel" in obj, get: obj => obj.updateChannel }, metadata: _metadata }, null, _instanceExtraInitializers);
|
|
125
135
|
__esDecorate(this, null, _addMember_decorators, { kind: "method", name: "addMember", static: false, private: false, access: { has: obj => "addMember" in obj, get: obj => obj.addMember }, metadata: _metadata }, null, _instanceExtraInitializers);
|
|
136
|
+
__esDecorate(this, null, _recoverMember_decorators, { kind: "method", name: "recoverMember", static: false, private: false, access: { has: obj => "recoverMember" in obj, get: obj => obj.recoverMember }, metadata: _metadata }, null, _instanceExtraInitializers);
|
|
137
|
+
__esDecorate(this, null, _restartMember_decorators, { kind: "method", name: "restartMember", static: false, private: false, access: { has: obj => "restartMember" in obj, get: obj => obj.restartMember }, metadata: _metadata }, null, _instanceExtraInitializers);
|
|
126
138
|
__esDecorate(this, null, _updateMember_decorators, { kind: "method", name: "updateMember", static: false, private: false, access: { has: obj => "updateMember" in obj, get: obj => obj.updateMember }, metadata: _metadata }, null, _instanceExtraInitializers);
|
|
127
139
|
__esDecorate(this, null, _changeTask_decorators, { kind: "method", name: "changeTask", static: false, private: false, access: { has: obj => "changeTask" in obj, get: obj => obj.changeTask }, metadata: _metadata }, null, _instanceExtraInitializers);
|
|
128
140
|
__esDecorate(this, null, _joinChannel_decorators, { kind: "method", name: "joinChannel", static: false, private: false, access: { has: obj => "joinChannel" in obj, get: obj => obj.joinChannel }, metadata: _metadata }, null, _instanceExtraInitializers);
|
|
129
141
|
__esDecorate(this, null, _removeChannelMember_decorators, { kind: "method", name: "removeChannelMember", static: false, private: false, access: { has: obj => "removeChannelMember" in obj, get: obj => obj.removeChannelMember }, metadata: _metadata }, null, _instanceExtraInitializers);
|
|
130
142
|
__esDecorate(this, null, _sendMessage_decorators, { kind: "method", name: "sendMessage", static: false, private: false, access: { has: obj => "sendMessage" in obj, get: obj => obj.sendMessage }, metadata: _metadata }, null, _instanceExtraInitializers);
|
|
143
|
+
__esDecorate(this, null, _putAttachment_decorators, { kind: "method", name: "putAttachment", static: false, private: false, access: { has: obj => "putAttachment" in obj, get: obj => obj.putAttachment }, metadata: _metadata }, null, _instanceExtraInitializers);
|
|
144
|
+
__esDecorate(this, null, _getAttachment_decorators, { kind: "method", name: "getAttachment", static: false, private: false, access: { has: obj => "getAttachment" in obj, get: obj => obj.getAttachment }, metadata: _metadata }, null, _instanceExtraInitializers);
|
|
131
145
|
__esDecorate(this, null, _reply_decorators, { kind: "method", name: "reply", static: false, private: false, access: { has: obj => "reply" in obj, get: obj => obj.reply }, metadata: _metadata }, null, _instanceExtraInitializers);
|
|
132
146
|
__esDecorate(this, null, _changeAttention_decorators, { kind: "method", name: "changeAttention", static: false, private: false, access: { has: obj => "changeAttention" in obj, get: obj => obj.changeAttention }, metadata: _metadata }, null, _instanceExtraInitializers);
|
|
133
147
|
__esDecorate(this, null, _inbox_decorators, { kind: "method", name: "inbox", static: false, private: false, access: { has: obj => "inbox" in obj, get: obj => obj.inbox }, metadata: _metadata }, null, _instanceExtraInitializers);
|
|
@@ -151,7 +165,19 @@ let AgentTeam = (() => {
|
|
|
151
165
|
handles = new Map();
|
|
152
166
|
diagnostics = new Map();
|
|
153
167
|
runtimeErrors = new Map();
|
|
168
|
+
/** Agent ids with a turn in flight; restarts must wait for the boundary. */
|
|
169
|
+
runningAgents = new Set();
|
|
154
170
|
notifiedInbox = new Map();
|
|
171
|
+
attachmentGcTimer;
|
|
172
|
+
recovery = new RecoveryCoordinator({
|
|
173
|
+
inject: (memberId, attempt, kind) => {
|
|
174
|
+
this.ctx.logger.info(`agent-team: automatic recovery steering member '${this.memberLabel(memberId)}' (attempt ${attempt}/${RECOVERY_MAX_ATTEMPTS}, ${kind})`);
|
|
175
|
+
this.injectRecovery(memberId, attempt, kind);
|
|
176
|
+
},
|
|
177
|
+
onStandDown: (memberId, attempts) => {
|
|
178
|
+
this.ctx.logger.warn(`agent-team: member '${this.memberLabel(memberId)}' still failing after ${attempts} automatic recovery attempts; leaving it in error for the operator`);
|
|
179
|
+
},
|
|
180
|
+
});
|
|
155
181
|
lifecycleTail = Promise.resolve();
|
|
156
182
|
accepting = true;
|
|
157
183
|
changeVersion = 0;
|
|
@@ -165,11 +191,20 @@ let AgentTeam = (() => {
|
|
|
165
191
|
const member = this.memberForAgent(agent);
|
|
166
192
|
if (member === undefined)
|
|
167
193
|
return;
|
|
168
|
-
|
|
194
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
195
|
+
this.runtimeErrors.set(agent.id, message);
|
|
196
|
+
const kind = classifyRecoverableError(message);
|
|
197
|
+
if (kind !== undefined)
|
|
198
|
+
this.ctx.logger.warn(`agent-team: member '${member.handle}' hit a recoverable error (${kind}); automatic recovery scheduled`);
|
|
199
|
+
this.recovery.onError(member.memberId, message);
|
|
169
200
|
this.emitChanged([{ kind: 'workspace', workspaceId: member.workspaceId }]);
|
|
170
201
|
});
|
|
171
202
|
this.ctx.on('agent/status', ({ agent, status }) => {
|
|
172
203
|
const member = this.memberForAgent(agent);
|
|
204
|
+
if (status === 'running')
|
|
205
|
+
this.runningAgents.add(agent.id);
|
|
206
|
+
else
|
|
207
|
+
this.runningAgents.delete(agent.id);
|
|
173
208
|
if (status === 'running' && member !== undefined) {
|
|
174
209
|
const recovered = this.runtimeErrors.delete(agent.id);
|
|
175
210
|
if (recovered)
|
|
@@ -177,14 +212,23 @@ let AgentTeam = (() => {
|
|
|
177
212
|
this.emitChanged([{ kind: 'workspace', workspaceId: member.workspaceId }]);
|
|
178
213
|
this.notifyMember(agent);
|
|
179
214
|
}
|
|
215
|
+
// A turn that ends without an error closes any automatic recovery episode.
|
|
216
|
+
if (status === 'idle' && member !== undefined && !this.runtimeErrors.has(agent.id)) {
|
|
217
|
+
this.recovery.onCleanTurnEnd(member.memberId);
|
|
218
|
+
}
|
|
180
219
|
});
|
|
181
220
|
const domain = await this.ctx.storageDomain.open(agentTeamDomainSpec);
|
|
182
221
|
this.ctx.effect(() => async () => {
|
|
183
222
|
this.accepting = false;
|
|
223
|
+
this.recovery.dispose();
|
|
224
|
+
if (this.attachmentGcTimer !== undefined)
|
|
225
|
+
clearInterval(this.attachmentGcTimer);
|
|
226
|
+
this.attachmentGcTimer = undefined;
|
|
184
227
|
this.emitChanged();
|
|
185
228
|
await this.lifecycleTail;
|
|
186
229
|
await Promise.all([...this.handles.values()].map(handle => handle.dispose()));
|
|
187
230
|
this.handles.clear();
|
|
231
|
+
this.runningAgents.clear();
|
|
188
232
|
await domain.close();
|
|
189
233
|
}, 'agentTeam.dispose');
|
|
190
234
|
this.domain = domain;
|
|
@@ -193,6 +237,7 @@ let AgentTeam = (() => {
|
|
|
193
237
|
const initialization = await ledger.initialize();
|
|
194
238
|
if (initialization.committed)
|
|
195
239
|
this.emitCommitted(initialization.value);
|
|
240
|
+
this.startAttachmentGc(ledger);
|
|
196
241
|
// One metadata listing serves every Member restore; per-member list calls
|
|
197
242
|
// would repeat the same I/O linearly during startup.
|
|
198
243
|
const persistedSessions = new Set((await this.ctx.sessionPersistence.list()).map(header => header.id));
|
|
@@ -293,6 +338,7 @@ let AgentTeam = (() => {
|
|
|
293
338
|
async addMember(request) {
|
|
294
339
|
return this.enqueueLifecycle(async () => {
|
|
295
340
|
const workspace = this.requireWorkspace(request.workspaceId);
|
|
341
|
+
await this.assertModelRoute(request.model);
|
|
296
342
|
const memberId = `member:${randomUUID()}`;
|
|
297
343
|
const member = Object.freeze({
|
|
298
344
|
memberId,
|
|
@@ -341,6 +387,129 @@ let AgentTeam = (() => {
|
|
|
341
387
|
return Object.freeze({ receipt: result.value.receipt, status: this.memberStatus(result.value.member) });
|
|
342
388
|
});
|
|
343
389
|
}
|
|
390
|
+
/**
|
|
391
|
+
* Operator nudge for an error-stopped Member: steer a continuation prompt
|
|
392
|
+
* into its live session. Runtime-only — no ledger operation, no suspend.
|
|
393
|
+
* Taking over manually also cancels any pending automatic recovery episode.
|
|
394
|
+
*/
|
|
395
|
+
async recoverMember(request) {
|
|
396
|
+
this.requireAccepting();
|
|
397
|
+
const member = this.requireLedger().getMember(request.memberId);
|
|
398
|
+
if (member === undefined || member.workspaceId !== request.workspaceId)
|
|
399
|
+
throw new Error(`unknown Member '${request.memberId}' in workspace '${request.workspaceId}'`);
|
|
400
|
+
const storedError = this.storedErrorFor(member.memberId);
|
|
401
|
+
this.recovery.stopTracking(request.memberId);
|
|
402
|
+
const kind = classifyRecoverableError(storedError ?? '');
|
|
403
|
+
this.ctx.logger.info(`agent-team: operator asked member '${member.handle}' to resume`);
|
|
404
|
+
this.steerResume(member, this.resumeText(kind ?? 'unspecified failure', { operatorRequested: true }));
|
|
405
|
+
return Object.freeze({ status: this.memberStatus(member) });
|
|
406
|
+
}
|
|
407
|
+
/**
|
|
408
|
+
* Restart one enabled Member's live session in place: dispose the handle and
|
|
409
|
+
* reactivate on the same session id, so the transcript, private memory, and
|
|
410
|
+
* every ledger fact survive while tools and prompt templates are re-minted
|
|
411
|
+
* from the running plugin. The audit operation changes no projection state.
|
|
412
|
+
*/
|
|
413
|
+
async restartMember(request) {
|
|
414
|
+
return this.enqueueLifecycle(async () => {
|
|
415
|
+
this.requireAccepting();
|
|
416
|
+
this.requireWorkspace(request.workspaceId);
|
|
417
|
+
const stored = this.requireLedger().getMember(request.memberId);
|
|
418
|
+
if (stored === undefined || stored.workspaceId !== request.workspaceId)
|
|
419
|
+
throw new Error(`unknown Member '${request.memberId}' in workspace '${request.workspaceId}'`);
|
|
420
|
+
if (stored.state !== 'enabled')
|
|
421
|
+
throw new Error(`Agent Member '${stored.handle}' is ${stored.state}; only enabled Members can be restarted`);
|
|
422
|
+
const active = this.handles.get(request.memberId);
|
|
423
|
+
if (active === undefined)
|
|
424
|
+
throw new Error(`Agent Member '${stored.handle}' has no active session to restart`);
|
|
425
|
+
if (this.runningAgents.has(active.agent.id))
|
|
426
|
+
throw new Error(`Agent Member '${stored.handle}' is still running; wait for the current turn to end before restarting`);
|
|
427
|
+
const result = await this.requireLedger().restartMember({ ...request, actor: agentTeamHumanActor() });
|
|
428
|
+
if (result.committed)
|
|
429
|
+
this.emitCommitted(result.value.receipt);
|
|
430
|
+
// Drop the old handle's transient state: pending recovery episodes and
|
|
431
|
+
// error markers belong to the disposed agent, not to the Member.
|
|
432
|
+
this.recovery.stopTracking(request.memberId);
|
|
433
|
+
this.runtimeErrors.delete(active.agent.id);
|
|
434
|
+
this.notifiedInbox.delete(request.memberId);
|
|
435
|
+
await active.dispose();
|
|
436
|
+
this.handles.delete(request.memberId);
|
|
437
|
+
this.diagnostics.delete(request.memberId);
|
|
438
|
+
await this.activateMember(stored);
|
|
439
|
+
const reactivated = this.handles.get(request.memberId);
|
|
440
|
+
if (reactivated === undefined) {
|
|
441
|
+
// Reactivation failed; the activation diagnostic carries the reason and
|
|
442
|
+
// the audit record stays honest about the attempt.
|
|
443
|
+
throw new Error(`Agent Member '${stored.handle}' failed to reactivate: ${this.diagnostics.get(request.memberId) ?? 'unknown error'}`);
|
|
444
|
+
}
|
|
445
|
+
return Object.freeze({ receipt: result.value.receipt, status: this.memberStatus(stored) });
|
|
446
|
+
});
|
|
447
|
+
}
|
|
448
|
+
/** Ledger handle for log lines; falls back to the raw id when unknown. */
|
|
449
|
+
memberLabel(memberId) {
|
|
450
|
+
return this.ledger?.getMember(memberId)?.handle ?? memberId;
|
|
451
|
+
}
|
|
452
|
+
/**
|
|
453
|
+
* Cache GC: uploads referenced by a Message survive 72h from upload so
|
|
454
|
+
* Member agents keep a consumption window; orphans (never sent) go after
|
|
455
|
+
* 24h. Runs once at startup and then daily — in-process only, because the
|
|
456
|
+
* cache is transient by design and rebuilds nothing across restarts.
|
|
457
|
+
*/
|
|
458
|
+
startAttachmentGc(ledger) {
|
|
459
|
+
const sweep = async () => {
|
|
460
|
+
await sweepAttachmentCache(attachmentsRoot(), ledger.referencedAttachmentIds(), Date.now());
|
|
461
|
+
};
|
|
462
|
+
void sweep();
|
|
463
|
+
this.attachmentGcTimer = setInterval(() => { void sweep(); }, 24 * 60 * 60 * 1000);
|
|
464
|
+
this.attachmentGcTimer.unref?.();
|
|
465
|
+
}
|
|
466
|
+
/** Latest runtime error summary for a Member's live session, if any. */
|
|
467
|
+
storedErrorFor(memberId) {
|
|
468
|
+
const handle = this.handles.get(memberId);
|
|
469
|
+
if (handle === undefined)
|
|
470
|
+
return undefined;
|
|
471
|
+
return this.runtimeErrors.get(handle.agent.id);
|
|
472
|
+
}
|
|
473
|
+
/**
|
|
474
|
+
* Continuation prompt per the recovery design: name the error family,
|
|
475
|
+
* annotate automatic attempts, and on the final attempt ask for a graceful
|
|
476
|
+
* handoff instead of another crash.
|
|
477
|
+
*/
|
|
478
|
+
resumeText(kindLabel, options) {
|
|
479
|
+
const opening = options?.operatorRequested === true ? 'The operator asked you to resume. ' : '';
|
|
480
|
+
const attemptNote = options?.attempt === undefined ? '' : ` (automatic recovery, attempt ${options.attempt}/${RECOVERY_MAX_ATTEMPTS})`;
|
|
481
|
+
let text = `${opening}Your previous turn ended early due to an error (${kindLabel})${attemptNote}. Please continue your work from where it stopped.`;
|
|
482
|
+
if (options?.attempt === RECOVERY_MAX_ATTEMPTS) {
|
|
483
|
+
text += ' If the same error recurs, stop and leave a short handoff note for the operator.';
|
|
484
|
+
}
|
|
485
|
+
return text;
|
|
486
|
+
}
|
|
487
|
+
/**
|
|
488
|
+
* Steer one continuation message into a Member's live session. Throws when
|
|
489
|
+
* no handle exists so the coordinator stops tracking; appends the inbox
|
|
490
|
+
* snapshot whenever there is anything new to read.
|
|
491
|
+
*/
|
|
492
|
+
steerResume(member, text) {
|
|
493
|
+
const handle = this.handles.get(member.memberId);
|
|
494
|
+
if (handle === undefined)
|
|
495
|
+
throw new Error(`member '${member.handle}' has no active session`);
|
|
496
|
+
const notifications = this.requireLedger().notificationFacts(member.memberId, { workspaceId: member.workspaceId });
|
|
497
|
+
const body = notifications.length === 0 ? text : `${text}\n\n${this.notificationText(notifications)}`;
|
|
498
|
+
const hint = createUserMessage({
|
|
499
|
+
content: [{ type: 'text', text: body }],
|
|
500
|
+
source: { kind: 'plugin', plugin: '@wowyuarm/dsh-agent-team', form: 'notice', summary: 'Recovery: continue your interrupted work.' },
|
|
501
|
+
});
|
|
502
|
+
handle.agent.steer(hint);
|
|
503
|
+
}
|
|
504
|
+
/** Automatic-recovery injection; throwing tells the coordinator the target is gone. */
|
|
505
|
+
injectRecovery(memberId, attempt, kind) {
|
|
506
|
+
const handle = this.handles.get(memberId);
|
|
507
|
+
const agent = handle?.agent;
|
|
508
|
+
const member = agent !== undefined ? this.memberForAgent(agent) : undefined;
|
|
509
|
+
if (agent === undefined || member === undefined || member.state !== 'enabled')
|
|
510
|
+
throw new Error(`member '${memberId}' cannot be recovered automatically`);
|
|
511
|
+
this.steerResume(member, this.resumeText(kind, { attempt }));
|
|
512
|
+
}
|
|
344
513
|
/**
|
|
345
514
|
* Human edit of one Member's mutable facts. A model change restarts an
|
|
346
515
|
* active Member: single-Session Members read their selection only at
|
|
@@ -349,6 +518,7 @@ let AgentTeam = (() => {
|
|
|
349
518
|
*/
|
|
350
519
|
async updateMember(request) {
|
|
351
520
|
return this.enqueueLifecycle(async () => {
|
|
521
|
+
await this.assertModelRoute(request.model);
|
|
352
522
|
const previous = this.requireLedger().getMember(request.memberId);
|
|
353
523
|
const result = await this.requireLedger().updateMember({ ...request, actor: agentTeamHumanActor() });
|
|
354
524
|
if (result.committed)
|
|
@@ -413,15 +583,84 @@ let AgentTeam = (() => {
|
|
|
413
583
|
async sendMessage(request) {
|
|
414
584
|
this.requireAccepting();
|
|
415
585
|
this.requireWorkspace(request.workspaceId);
|
|
416
|
-
const
|
|
586
|
+
const metadata = await this.resolveMessageAttachments(request);
|
|
587
|
+
const result = await this.requireLedger().sendMessage({
|
|
588
|
+
...request, body: this.appendAttachmentLines(request.body, metadata),
|
|
589
|
+
...(metadata.length === 0 ? {} : { resolvedAttachments: metadata }),
|
|
590
|
+
actor: agentTeamHumanActor(),
|
|
591
|
+
});
|
|
417
592
|
this.emitCommittedOutcome(result);
|
|
418
593
|
return result.value;
|
|
419
594
|
}
|
|
595
|
+
/**
|
|
596
|
+
* Resolve uploaded ids and agent-supplied absolute paths into one attachment
|
|
597
|
+
* metadata list. Paths are all validated before anything is copied, so one
|
|
598
|
+
* rejection leaves the cache untouched and the message uncommitted.
|
|
599
|
+
*/
|
|
600
|
+
async resolveMessageAttachments(request) {
|
|
601
|
+
const fromPaths = [];
|
|
602
|
+
if (request.attachmentPaths !== undefined && request.attachmentPaths.length > 0) {
|
|
603
|
+
for (const absolutePath of request.attachmentPaths)
|
|
604
|
+
await validatePathAttachment(absolutePath);
|
|
605
|
+
for (const absolutePath of request.attachmentPaths)
|
|
606
|
+
fromPaths.push(Object.freeze(await copyPathAttachment(attachmentsRoot(), absolutePath)));
|
|
607
|
+
}
|
|
608
|
+
return [...fromPaths, ...await this.prepareAttachments(request.attachments)];
|
|
609
|
+
}
|
|
610
|
+
/** Verify requested attachment ids against the cache. */
|
|
611
|
+
async prepareAttachments(requested) {
|
|
612
|
+
if (requested === undefined || requested.length === 0)
|
|
613
|
+
return [];
|
|
614
|
+
const metadata = [];
|
|
615
|
+
for (const attachmentId of requested) {
|
|
616
|
+
const stored = await readAttachment(attachmentsRoot(), attachmentId);
|
|
617
|
+
if (stored === undefined)
|
|
618
|
+
throw new Error(`attachment '${attachmentId}' is not in the upload cache`);
|
|
619
|
+
metadata.push(Object.freeze({ attachmentId, name: stored.name, byteSize: stored.byteSize, mediaType: stored.mediaType }));
|
|
620
|
+
}
|
|
621
|
+
return metadata;
|
|
622
|
+
}
|
|
623
|
+
/**
|
|
624
|
+
* Derive the stored body: one machine-facing `[attachment] <absolute path>`
|
|
625
|
+
* line per attachment appended to the member-facing text.
|
|
626
|
+
*/
|
|
627
|
+
appendAttachmentLines(body, metadata) {
|
|
628
|
+
const trimmed = body.trim();
|
|
629
|
+
if (metadata.length === 0)
|
|
630
|
+
return trimmed;
|
|
631
|
+
const lines = metadata.map(attachment => `[attachment] ${dshHomePath('agent-team', 'attachments', 'v1', attachment.attachmentId, attachment.name)}`);
|
|
632
|
+
return `${trimmed}\n${lines.join('\n')}`;
|
|
633
|
+
}
|
|
634
|
+
/** Upload one composer attachment into the cache; bytes are immutable once written. */
|
|
635
|
+
async putAttachment(request) {
|
|
636
|
+
this.requireAccepting();
|
|
637
|
+
this.requireWorkspace(request.workspaceId);
|
|
638
|
+
const bytes = Buffer.from(request.bytesBase64, 'base64');
|
|
639
|
+
if (bytes.byteLength === 0)
|
|
640
|
+
throw new Error('attachment must not be empty');
|
|
641
|
+
if (bytes.byteLength > ATTACHMENT_MAX_BYTES)
|
|
642
|
+
throw new Error(`attachment exceeds the ${ATTACHMENT_MAX_BYTES} byte limit`);
|
|
643
|
+
const mediaType = sanitizeMediaType(request.mediaType);
|
|
644
|
+
const attachmentId = newAttachmentId();
|
|
645
|
+
return Object.freeze(await writeAttachment(attachmentsRoot(), attachmentId, request.name, mediaType, bytes));
|
|
646
|
+
}
|
|
647
|
+
/** Read one cached attachment back for client display; gone entries throw and the UI degrades to a chip. */
|
|
648
|
+
async getAttachment(request) {
|
|
649
|
+
const stored = await readAttachment(attachmentsRoot(), request.attachmentId);
|
|
650
|
+
if (stored === undefined)
|
|
651
|
+
throw new Error(`attachment '${request.attachmentId}' is no longer cached`);
|
|
652
|
+
return Object.freeze({ name: stored.name, mediaType: stored.mediaType, byteSize: stored.byteSize, bytesBase64: stored.bytes.toString('base64') });
|
|
653
|
+
}
|
|
420
654
|
/** Human existing-Thread reply; unread and revision conflicts are business outcomes. */
|
|
421
655
|
async reply(request) {
|
|
422
656
|
this.requireAccepting();
|
|
423
657
|
this.requireWorkspace(request.workspaceId);
|
|
424
|
-
const
|
|
658
|
+
const metadata = await this.resolveMessageAttachments(request);
|
|
659
|
+
const result = await this.requireLedger().reply({
|
|
660
|
+
...request, body: this.appendAttachmentLines(request.body, metadata),
|
|
661
|
+
...(metadata.length === 0 ? {} : { resolvedAttachments: metadata }),
|
|
662
|
+
actor: agentTeamHumanActor(),
|
|
663
|
+
});
|
|
425
664
|
this.emitCommittedOutcome(result);
|
|
426
665
|
return result.value;
|
|
427
666
|
}
|
|
@@ -468,7 +707,12 @@ let AgentTeam = (() => {
|
|
|
468
707
|
this.requireAccepting();
|
|
469
708
|
const actor = this.memberActor(agent);
|
|
470
709
|
this.requireAgentWorkspace(actor, request.workspaceId);
|
|
471
|
-
const
|
|
710
|
+
const metadata = await this.resolveMessageAttachments(request);
|
|
711
|
+
const result = await this.requireLedger().sendMessage({
|
|
712
|
+
...request, body: this.appendAttachmentLines(request.body, metadata),
|
|
713
|
+
...(metadata.length === 0 ? {} : { resolvedAttachments: metadata }),
|
|
714
|
+
actor,
|
|
715
|
+
});
|
|
472
716
|
this.emitCommittedOutcome(result);
|
|
473
717
|
return result.value;
|
|
474
718
|
}
|
|
@@ -477,7 +721,12 @@ let AgentTeam = (() => {
|
|
|
477
721
|
this.requireAccepting();
|
|
478
722
|
const actor = this.memberActor(agent);
|
|
479
723
|
this.requireAgentWorkspace(actor, request.workspaceId);
|
|
480
|
-
const
|
|
724
|
+
const metadata = await this.resolveMessageAttachments(request);
|
|
725
|
+
const result = await this.requireLedger().reply({
|
|
726
|
+
...request, body: this.appendAttachmentLines(request.body, metadata),
|
|
727
|
+
...(metadata.length === 0 ? {} : { resolvedAttachments: metadata }),
|
|
728
|
+
actor,
|
|
729
|
+
});
|
|
481
730
|
this.emitCommittedOutcome(result);
|
|
482
731
|
return result.value;
|
|
483
732
|
}
|
|
@@ -565,6 +814,26 @@ let AgentTeam = (() => {
|
|
|
565
814
|
if (this.requireLedger().getMember(actor.memberId)?.workspaceId !== workspaceId)
|
|
566
815
|
throw new Error('Member cannot mutate another Workspace');
|
|
567
816
|
}
|
|
817
|
+
/**
|
|
818
|
+
* Validate a pinned model route's reasoning effort against the adapter's own
|
|
819
|
+
* metadata when the LLM service is reachable; unknown routes defer to the
|
|
820
|
+
* LLM layer's runtime check at call time.
|
|
821
|
+
*/
|
|
822
|
+
async assertModelRoute(model) {
|
|
823
|
+
if (model === undefined || model.reasoningEffort === undefined)
|
|
824
|
+
return;
|
|
825
|
+
try {
|
|
826
|
+
const resolved = await this.ctx.llm.resolveModelInfo(model.provider, model.model);
|
|
827
|
+
const efforts = resolved.reasoning?.efforts ?? [];
|
|
828
|
+
if (efforts.length > 0 && !efforts.some(effort => effort.id === model.reasoningEffort)) {
|
|
829
|
+
throw new Error(`reasoning effort '${model.reasoningEffort}' is not supported by ${model.provider}/${model.model}`);
|
|
830
|
+
}
|
|
831
|
+
}
|
|
832
|
+
catch (error) {
|
|
833
|
+
if (error instanceof Error && error.message.includes('is not supported by'))
|
|
834
|
+
throw error;
|
|
835
|
+
}
|
|
836
|
+
}
|
|
568
837
|
async activateMember(member, knownWorkspacePath, knownSessions) {
|
|
569
838
|
if (this.handles.has(member.memberId))
|
|
570
839
|
return;
|