agentworkshop 0.5.5 → 0.5.6
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/.AgentWorkShop/prompts/compaction-hints.md +9 -0
- package/.AgentWorkShop/prompts/host-tools.json +664 -630
- package/.AgentWorkShop/prompts/peer-reply-cross-channel.md +10 -0
- package/.AgentWorkShop/prompts/system-manual.md +6 -1
- package/.output/.AgentWorkShop/prompts/compaction-hints.md +9 -0
- package/.output/.AgentWorkShop/prompts/host-tools.json +664 -630
- package/.output/.AgentWorkShop/prompts/peer-reply-cross-channel.md +10 -0
- package/.output/.AgentWorkShop/prompts/system-manual.md +6 -1
- package/.output/nitro.json +1 -1
- package/.output/public/_nuxt/builds/latest.json +1 -1
- package/.output/public/_nuxt/builds/meta/d50c6ec2-801c-4ba2-97d8-a4db401b702c.json +1 -0
- package/.output/server/chunks/_/nitro.mjs +1920 -1211
- package/.output/server/chunks/_/nitro.mjs.map +1 -1
- package/.output/server/chunks/routes/api/workshop/channels/_id/index3.post.mjs +25 -3
- package/.output/server/chunks/routes/api/workshop/channels/_id/index3.post.mjs.map +1 -1
- package/.output/server/package.json +1 -1
- package/package.json +1 -1
- package/scripts/_dbg-cross-channel-e2e.mjs +157 -0
- package/scripts/test-context-governor.ts +176 -0
- package/scripts/test-memory-brief-chronicle.ts +180 -0
- package/scripts/test-memory-dynamic.ts +3 -1
- package/scripts/test-memory-rrf-mmr.ts +114 -0
- package/server/api/workshop/channels/[id]/messages/index.post.ts +26 -1
- package/server/api/workshop/ws.ts +1 -0
- package/server/data/daqs.json +35 -13
- package/server/data/dcw-lines.json +14 -0
- package/server/data/dcw-products.json +14 -0
- package/server/data/dcw-recipes.json +28 -0
- package/server/data/dcw-rollback.json +122 -1
- package/server/data/dcw-runs.json +54 -36
- package/server/data/dcw-writes.json +60 -0
- package/server/data/dcws.json +42 -0
- package/server/data/line-runs.json +32 -22
- package/server/services/workshop/agents/adapters/omp-rpc-client.ts +65 -0
- package/server/services/workshop/agents/agent-interface.ts +26 -1
- package/server/services/workshop/agents/mock-agent.ts +12 -0
- package/server/services/workshop/agents/omp-agent.ts +277 -8
- package/server/services/workshop/db/database.ts +16 -1
- package/server/services/workshop/db/memory.repo.ts +22 -2
- package/server/services/workshop/prompts/loader.ts +6 -5
- package/server/services/workshop/runtime/agent-runtime.ts +52 -6
- package/server/services/workshop/runtime/channel-runtime.ts +14 -6
- package/server/services/workshop/runtime/manager.ts +192 -3
- package/server/services/workshop/runtime/memory.ts +313 -78
- package/server/services/workshop/types/task.ts +14 -0
- package/.output/public/_nuxt/builds/meta/f2f649c5-fd3b-4507-a3a1-8b4a194de72d.json +0 -1
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* 权威契约见 docs/superpowers/plans/2026-08-13-agent-workshop-multi-agent.md 核心契约块。
|
|
6
6
|
*/
|
|
7
7
|
import type { A2AMessage, A2AArtifact, A2AError, ChannelMail, Part } from '../types/a2a'
|
|
8
|
-
import type { WorkspaceTask, AgentTaskQueueView, AgentStatusView } from '../types/task'
|
|
8
|
+
import type { WorkspaceTask, AgentTaskQueueView, AgentStatusView, AgentContextStats } from '../types/task'
|
|
9
9
|
|
|
10
10
|
/** Agent 元信息:Channel 内成员声明(hook harness 类型与配置) */
|
|
11
11
|
export interface AgentInfo {
|
|
@@ -59,6 +59,16 @@ export interface AgentWorkspace {
|
|
|
59
59
|
cancelTask(taskId: string): Promise<WorkspaceTask>
|
|
60
60
|
/** 点对点发消息给同事 */
|
|
61
61
|
sendMessage(input: { toAgentId: string, parts: Part[], metadata?: Record<string, unknown> }): Promise<A2AMessage>
|
|
62
|
+
/**
|
|
63
|
+
* (仅 lead)跨 Channel 点对点:直投目标 channel 的 lead mailbox(沿用 mailbox/route 机制)。
|
|
64
|
+
* 权限:发送方必须是本 channel 的 lead;目标 channel 与本 channel 须同主且启用、有 lead。
|
|
65
|
+
*/
|
|
66
|
+
sendCrossChannelMessage(input: { toChannelId: string, parts: Part[], requireReply?: boolean, inReplyTo?: string }): Promise<{
|
|
67
|
+
messageId: string
|
|
68
|
+
toChannelId: string
|
|
69
|
+
toChannelName: string
|
|
70
|
+
toLeadAgentId: string
|
|
71
|
+
}>
|
|
62
72
|
/**
|
|
63
73
|
* 拒绝指派给自己的任务(能力/范畴不匹配):任务置 FAILED(调度器改派他人),
|
|
64
74
|
* 并向派发方(任务创建者,缺省 channel lead)回执拒绝原因。
|
|
@@ -91,6 +101,11 @@ export interface AgentWorkspace {
|
|
|
91
101
|
* scope='private' → 本人记忆库;scope='shared' → Channel 公共记忆域(全员可检索)。
|
|
92
102
|
*/
|
|
93
103
|
saveMemory(input: { title: string, content: string, importance?: number, scope: 'private' | 'shared', dedupKey?: string }): Promise<{ scope: 'private' | 'shared', dedupKey: string }>
|
|
104
|
+
/**
|
|
105
|
+
* 会话压缩摘要入库(harvest 桥;omp compaction 产出的会话摘要 → 本成员持久记忆)。
|
|
106
|
+
* 平台 70% 主动压缩与 omp 原生 auto-compaction 双路统一经此落库(episodic-session)。
|
|
107
|
+
*/
|
|
108
|
+
recordSessionMemory?(input: { summary: string, tokensBefore?: number, tokensAfter?: number, reason?: string }): Promise<void>
|
|
94
109
|
/** 订阅同事产出 */
|
|
95
110
|
subscribe(input: { agentIds?: string[] }): Promise<void>
|
|
96
111
|
/** (lead)在本 channel 内新建团队成员(worker):按需建模板并克隆为独立实例 */
|
|
@@ -216,4 +231,14 @@ export interface AgentInterface {
|
|
|
216
231
|
* 探到进程已死时收敛为已退出,让在途回合归位、下一回合自动重生子进程。
|
|
217
232
|
*/
|
|
218
233
|
reconcileProcess?(): void
|
|
234
|
+
/**
|
|
235
|
+
* 可选:harness 上下文用量快照(omp;被动缓存值,无探测 RPC)。
|
|
236
|
+
* 进程内 harness(mock/claude)不实现 → getStatus 不透出 context 字段。
|
|
237
|
+
*/
|
|
238
|
+
getContextStats?(): AgentContextStats | null
|
|
239
|
+
/**
|
|
240
|
+
* 可选:回合落定后的后台钩子(post-settle 上下文压缩检查)。
|
|
241
|
+
* 实现方自守卫(仅回合间隙发起、异常不抛出);调用方 AgentRuntime 在信箱无排队消息时调用。
|
|
242
|
+
*/
|
|
243
|
+
onTurnSettled?(): Promise<void>
|
|
219
244
|
}
|
|
@@ -352,6 +352,18 @@ export class MockAgentImpl implements AgentInterface {
|
|
|
352
352
|
const text = request.message.parts
|
|
353
353
|
.map(p => ('text' in p ? p.text : 'data' in p ? JSON.stringify(p.data) : ''))
|
|
354
354
|
.join('\n')
|
|
355
|
+
// 跨 Channel 来信:发送方不是本 channel 成员,回复须走 lead 级跨 Channel 发送(回源 channel)
|
|
356
|
+
const crossChannel = request.message.metadata?.['x-aw-cross-channel'] === 'true'
|
|
357
|
+
const fromChannel = request.message.metadata?.['x-aw-from-channel']
|
|
358
|
+
if (crossChannel && typeof fromChannel === 'string') {
|
|
359
|
+
await ctx.workspace.sendCrossChannelMessage({
|
|
360
|
+
toChannelId: fromChannel,
|
|
361
|
+
parts: [{ text: `mock 跨Channel回复(${ctx.agentId}):已处理「${text.slice(0, 60)}」。执行结果:完成;你所需的内容已包含在本回复中。本回复不需要再响应。` }],
|
|
362
|
+
inReplyTo: request.message.messageId,
|
|
363
|
+
})
|
|
364
|
+
yield { kind: 'done' }
|
|
365
|
+
return
|
|
366
|
+
}
|
|
355
367
|
await ctx.workspace.sendMessage({
|
|
356
368
|
toAgentId: fromId,
|
|
357
369
|
parts: [{ text: `mock 回复(${ctx.agentId}):已处理「${text.slice(0, 60)}」。执行结果:完成;你所需的内容已包含在本回复中。本回复不需要再响应。` }],
|
|
@@ -28,10 +28,11 @@ import type {
|
|
|
28
28
|
SupervisionSnapshot,
|
|
29
29
|
} from './agent-interface'
|
|
30
30
|
import type { A2AArtifact, A2AMessage, Part } from '../types/a2a'
|
|
31
|
-
import type { WorkspaceTask } from '../types/task'
|
|
31
|
+
import type { AgentContextStats, WorkspaceTask } from '../types/task'
|
|
32
32
|
import {
|
|
33
33
|
OmpRpcClient,
|
|
34
34
|
type AgentSessionEvent,
|
|
35
|
+
type CompactionResult,
|
|
35
36
|
type RpcHostToolDefinition,
|
|
36
37
|
type HostToolCallRequest,
|
|
37
38
|
} from './adapters/omp-rpc-client'
|
|
@@ -52,6 +53,7 @@ import {
|
|
|
52
53
|
import { toolDaqQuery, toolDcwControl, toolDcwJudge, toolDcwJournal, toolDcwRollback, toolMyIndustrialNodes } from './industrial-tools'
|
|
53
54
|
import { buildIndustrialContext, industrialLoopGuide } from './industrial-context'
|
|
54
55
|
import { extractTaskMode } from '../runtime/execution-mode'
|
|
56
|
+
import { envNum } from '../runtime/memory'
|
|
55
57
|
|
|
56
58
|
const log = createLogger('workshop.omp')
|
|
57
59
|
|
|
@@ -217,6 +219,194 @@ export class OmpRpcAgentImpl implements AgentInterface {
|
|
|
217
219
|
*/
|
|
218
220
|
private replyContext: { fromId: string, messageId: string } | null = null
|
|
219
221
|
|
|
222
|
+
// ===== 上下文治理(70% 无中断压缩环)=====
|
|
223
|
+
/** 压缩进行中(平台 gate 与 omp 原生压缩共用互斥位) */
|
|
224
|
+
private compacting = false
|
|
225
|
+
private lastCompactAt = 0
|
|
226
|
+
/** omp 不支持 compact 命令(旧版)→ 永久停用平台压缩;原生 auto-compaction 兜底,harvest 照常 */
|
|
227
|
+
private compactLegacy = false
|
|
228
|
+
/** 模型上下文窗口(get_state/get_session_stats 探测;null = 未知,percent 不可算) */
|
|
229
|
+
private contextWindow: number | null = null
|
|
230
|
+
private sessionId: string | null = null
|
|
231
|
+
/** harvest 双路去重(compact 响应与 compaction_end 事件可能双达) */
|
|
232
|
+
private lastHarvestKey = ''
|
|
233
|
+
private lastHarvestAt = 0
|
|
234
|
+
|
|
235
|
+
/** 总开关(AW_OMP_COMPACT_ENABLED=0 显式关闭;envNum 拒 0,故单独解析) */
|
|
236
|
+
private compactEnabled(): boolean {
|
|
237
|
+
const v = Number(process.env.AW_OMP_COMPACT_ENABLED)
|
|
238
|
+
return !(Number.isFinite(v) && v === 0)
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
private compactThreshold(): number {
|
|
242
|
+
return Math.min(1, envNum('AW_OMP_COMPACT_THRESHOLD', 0.7))
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
private compactMinIntervalMs(): number {
|
|
246
|
+
return envNum('AW_OMP_COMPACT_MIN_INTERVAL_MS', 300_000)
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
private compactWaitMs(): number {
|
|
250
|
+
return envNum('AW_OMP_COMPACT_WAIT_MS', 120_000)
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/** 被动上下文用量快照(无探测 RPC;getStatus 透出用) */
|
|
254
|
+
getContextStats(): AgentContextStats | null {
|
|
255
|
+
const usage = this.client?.getContextUsage() ?? null
|
|
256
|
+
const window = this.contextWindow ?? usage?.contextWindow ?? null
|
|
257
|
+
if (!usage && window === null) return null
|
|
258
|
+
const usedTokens = usage?.tokens ?? 0
|
|
259
|
+
return {
|
|
260
|
+
usedTokens,
|
|
261
|
+
contextWindow: window,
|
|
262
|
+
percent: window && window > 0 ? Math.min(1, usedTokens / window) : null,
|
|
263
|
+
compacting: this.compacting,
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/**
|
|
268
|
+
* 上下文用量探测:get_session_stats 权威值;不可用退化为被动 usage + 已探测窗口。
|
|
269
|
+
* percent 归一化到 0-1(omp 可能返回 0-100)。
|
|
270
|
+
*/
|
|
271
|
+
private async probeUsage(client: OmpRpcClient): Promise<{ tokens: number, contextWindow: number | null, percent: number | null } | null> {
|
|
272
|
+
try {
|
|
273
|
+
const resp = await client.send({ type: 'get_session_stats' })
|
|
274
|
+
const cu = ((resp.data ?? {}) as Record<string, unknown>).contextUsage as
|
|
275
|
+
| { tokens?: number, contextWindow?: number, percent?: number }
|
|
276
|
+
| undefined
|
|
277
|
+
if (cu && typeof cu.tokens === 'number' && cu.tokens > 0) {
|
|
278
|
+
if (typeof cu.contextWindow === 'number' && cu.contextWindow > 0) {
|
|
279
|
+
this.contextWindow = cu.contextWindow
|
|
280
|
+
client.setContextWindow(cu.contextWindow)
|
|
281
|
+
}
|
|
282
|
+
const window = this.contextWindow ?? cu.contextWindow ?? null
|
|
283
|
+
const rawPercent = typeof cu.percent === 'number' ? (cu.percent > 1 ? cu.percent / 100 : cu.percent) : null
|
|
284
|
+
const percent = rawPercent ?? (window && window > 0 ? Math.min(1, cu.tokens / window) : null)
|
|
285
|
+
return { tokens: cu.tokens, contextWindow: window, percent }
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
catch { /* get_session_stats 不可用 → 被动跟踪 */ }
|
|
289
|
+
return client.getContextUsage()
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
/**
|
|
293
|
+
* 上下文治理门控:三条 prompt 路径(worker/peer/supervise)在回合间隙统一调用;
|
|
294
|
+
* post-settle 经 onTurnSettled 复用同一守卫。任何失败路径都放行——压缩晚一轮
|
|
295
|
+
* 永远好过中断作业。三重防线:仅回合间隙发起 → get_state 复查 isStreaming/
|
|
296
|
+
* isCompacting → compacting 互斥位 + 最小间隔防振荡。
|
|
297
|
+
*/
|
|
298
|
+
private async contextGate(reason: string): Promise<void> {
|
|
299
|
+
if (!this.compactEnabled() || this.compactLegacy || this.compacting) return
|
|
300
|
+
const client = this.client
|
|
301
|
+
if (!client || !client.alive) return
|
|
302
|
+
try {
|
|
303
|
+
const usage = await this.probeUsage(client)
|
|
304
|
+
if (!usage || usage.percent === null || usage.percent < this.compactThreshold()) return
|
|
305
|
+
if (Date.now() - this.lastCompactAt < this.compactMinIntervalMs()) return
|
|
306
|
+
// 回合间隙硬校验:流式中/压缩中绝不发起(不中断执行的硬约束)
|
|
307
|
+
const st = await client.send({ type: 'get_state' })
|
|
308
|
+
const s = (st.data ?? {}) as Record<string, unknown>
|
|
309
|
+
if (s.isStreaming === true || s.isCompacting === true) return
|
|
310
|
+
if (typeof s.sessionId === 'string' && s.sessionId) this.sessionId = s.sessionId
|
|
311
|
+
this.compacting = true
|
|
312
|
+
this.lastCompactAt = Date.now()
|
|
313
|
+
try {
|
|
314
|
+
const result = await this.runCompaction(client)
|
|
315
|
+
if (result?.summary) {
|
|
316
|
+
await this.harvestCompaction(
|
|
317
|
+
result.summary,
|
|
318
|
+
result.tokensBefore,
|
|
319
|
+
result.tokensAfter ?? result.estimatedTokensAfter,
|
|
320
|
+
reason,
|
|
321
|
+
)
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
finally {
|
|
325
|
+
this.compacting = false
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
catch (err) {
|
|
329
|
+
const msg = err instanceof Error ? err.message : String(err)
|
|
330
|
+
if (/compact/i.test(msg) && /unknown|unsupported|invalid|无效|未知|不支持/i.test(msg)) this.compactLegacy = true
|
|
331
|
+
log.warn(`[OmpRpcAgent:${this.selfAgentId}] contextGate(${reason}) 失败,放行: ${msg}`)
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
/**
|
|
336
|
+
* 发起 compact 并等待 compaction_end(响应内含 result 则立即收口;
|
|
337
|
+
* 超时放行——压缩在 harness 内后台继续,isCompacting 防后续双发)。
|
|
338
|
+
*/
|
|
339
|
+
private runCompaction(client: OmpRpcClient): Promise<CompactionResult | null> {
|
|
340
|
+
return new Promise<CompactionResult | null>((resolve) => {
|
|
341
|
+
let settled = false
|
|
342
|
+
const finish = (r: CompactionResult | null): void => {
|
|
343
|
+
if (settled) return
|
|
344
|
+
settled = true
|
|
345
|
+
off()
|
|
346
|
+
clearTimeout(timer)
|
|
347
|
+
resolve(r)
|
|
348
|
+
}
|
|
349
|
+
const off = client.onEvent((event) => {
|
|
350
|
+
if (event.type === 'compaction_end') {
|
|
351
|
+
finish((event as { result?: CompactionResult }).result ?? null)
|
|
352
|
+
}
|
|
353
|
+
else if (event.type === '__process_exit__') {
|
|
354
|
+
finish(null)
|
|
355
|
+
}
|
|
356
|
+
})
|
|
357
|
+
const timer = setTimeout(() => {
|
|
358
|
+
log.warn(`[OmpRpcAgent:${this.selfAgentId}] compact 等待超时(${this.compactWaitMs()}ms)→ 放行(压缩后台继续)`)
|
|
359
|
+
finish(null)
|
|
360
|
+
}, this.compactWaitMs())
|
|
361
|
+
void client.send({ type: 'compact', customInstructions: this.compactionHints() })
|
|
362
|
+
.then((resp) => {
|
|
363
|
+
const result = (resp.data as { result?: CompactionResult } | undefined)?.result
|
|
364
|
+
if (result?.summary) finish(result)
|
|
365
|
+
// 响应无 result:等 compaction_end 事件(协议权威语义)
|
|
366
|
+
})
|
|
367
|
+
.catch((err) => {
|
|
368
|
+
const msg = err instanceof Error ? err.message : String(err)
|
|
369
|
+
if (/compact/i.test(msg)) this.compactLegacy = true
|
|
370
|
+
log.warn(`[OmpRpcAgent:${this.selfAgentId}] compact 发起失败: ${msg}`)
|
|
371
|
+
finish(null)
|
|
372
|
+
})
|
|
373
|
+
})
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
/** 压缩摘要指令(外置 prompts;缺失时省略,omp 用默认策略) */
|
|
377
|
+
private compactionHints(): string | undefined {
|
|
378
|
+
try {
|
|
379
|
+
return renderPrompt('compaction-hints')
|
|
380
|
+
}
|
|
381
|
+
catch {
|
|
382
|
+
return undefined
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
/**
|
|
387
|
+
* 压缩摘要 harvest(三路统一:平台主动 compact / omp 阈值自动 / overflow):
|
|
388
|
+
* 经 workspace.recordSessionMemory 落库为本人 episodic-session 记忆。
|
|
389
|
+
* 双路去重:同摘要 60s 内只入库一次。
|
|
390
|
+
*/
|
|
391
|
+
private async harvestCompaction(summary: string, tokensBefore?: number, tokensAfter?: number, reason?: string): Promise<void> {
|
|
392
|
+
const key = summary.slice(0, 120)
|
|
393
|
+
if (key === this.lastHarvestKey && Date.now() - this.lastHarvestAt < 60_000) return
|
|
394
|
+
this.lastHarvestKey = key
|
|
395
|
+
this.lastHarvestAt = Date.now()
|
|
396
|
+
try {
|
|
397
|
+
await this.workspace?.recordSessionMemory?.({ summary, tokensBefore, tokensAfter, reason })
|
|
398
|
+
log.info(`[OmpRpcAgent:${this.selfAgentId}] 压缩摘要已入记忆(${summary.length} 字,reason=${reason ?? 'auto'})`)
|
|
399
|
+
}
|
|
400
|
+
catch (err) {
|
|
401
|
+
log.warn(`[OmpRpcAgent:${this.selfAgentId}] 压缩摘要入库失败(不影响会话):`, err instanceof Error ? err.message : err)
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
/** 回合落定钩子(AgentRuntime 在信箱空时调用):后台压缩检查,自守卫不抛错 */
|
|
406
|
+
async onTurnSettled(): Promise<void> {
|
|
407
|
+
await this.contextGate('post-settle')
|
|
408
|
+
}
|
|
409
|
+
|
|
220
410
|
constructor(config: Record<string, unknown> = {}) {
|
|
221
411
|
this.config = config as OmpAgentConfig
|
|
222
412
|
// factory 注入的 agent 身份(无需等待 init())
|
|
@@ -449,6 +639,9 @@ export class OmpRpcAgentImpl implements AgentInterface {
|
|
|
449
639
|
if (!this.client) return []
|
|
450
640
|
if (this.supervising) return [] // 上一轮 supervise 未收口:跳过本拍(节流即正确)
|
|
451
641
|
|
|
642
|
+
// 上下文门控(≥70% 先压缩再调度;回合间隙发起,失败放行)
|
|
643
|
+
await this.contextGate('supervise')
|
|
644
|
+
|
|
452
645
|
const prompt = await this.buildSupervisePrompt(snapshot, ctx.memory)
|
|
453
646
|
// 150s 上界:supervise 持 lead.execLock 期间信箱消费停顿(更久会拖垮 worker 回执处理);
|
|
454
647
|
// supervise 是一次真实 LLM 回合:omp 冷启动(插件/MCP 加载 30~90s)+ 慢 provider
|
|
@@ -589,9 +782,14 @@ export class OmpRpcAgentImpl implements AgentInterface {
|
|
|
589
782
|
const msgText = partsToText(msg.parts)
|
|
590
783
|
const requireReply = msg.metadata?.['x-aw-require-reply'] === 'true'
|
|
591
784
|
const isReply = typeof msg.metadata?.['x-aw-in-reply-to'] === 'string'
|
|
785
|
+
// 跨 Channel 来信:发送方是其他 channel 的 lead,回执必须走 send_cross_channel_message
|
|
786
|
+
const crossChannel = msg.metadata?.['x-aw-cross-channel'] === 'true'
|
|
787
|
+
const fromChannel = typeof msg.metadata?.['x-aw-from-channel'] === 'string'
|
|
788
|
+
? String(msg.metadata['x-aw-from-channel'])
|
|
789
|
+
: ''
|
|
592
790
|
// 回执自动关联仅对真实 agent 发送者生效(人类无 agentId 可回投;
|
|
593
791
|
// 对人类的回复经事件流/时间线可见)
|
|
594
|
-
this.replyContext = requireReply && request.fromAgentId
|
|
792
|
+
this.replyContext = requireReply && request.fromAgentId && !crossChannel
|
|
595
793
|
? { fromId: request.fromAgentId, messageId: msg.messageId }
|
|
596
794
|
: null
|
|
597
795
|
|
|
@@ -599,11 +797,13 @@ export class OmpRpcAgentImpl implements AgentInterface {
|
|
|
599
797
|
? `You are "${this.agentName}", the LEAD coordinator of a multi-agent team (Channel: ${this.channelId}). A team member sent you a direct message.`
|
|
600
798
|
: `You are "${this.agentName}", a worker agent in a multi-agent team (Channel: ${this.channelId}).`
|
|
601
799
|
|
|
602
|
-
//
|
|
603
|
-
const respondBlock =
|
|
604
|
-
fromId,
|
|
605
|
-
|
|
606
|
-
|
|
800
|
+
// 消息应答指令:必复/可选/跨 Channel 三态 + lead 名册提示(全部外置 prompts/)
|
|
801
|
+
const respondBlock = crossChannel
|
|
802
|
+
? renderPrompt('peer-reply-cross-channel', { fromId, messageId: msg.messageId, fromChannel })
|
|
803
|
+
: renderPrompt(requireReply ? 'peer-reply-required' : 'peer-reply-optional', {
|
|
804
|
+
fromId,
|
|
805
|
+
messageId: msg.messageId,
|
|
806
|
+
})
|
|
607
807
|
const peerBody = renderPrompt('peer-message', {
|
|
608
808
|
fromId,
|
|
609
809
|
messageId: msg.messageId,
|
|
@@ -647,6 +847,8 @@ export class OmpRpcAgentImpl implements AgentInterface {
|
|
|
647
847
|
if (!client) {
|
|
648
848
|
throw new Error('omp 客户端未就绪')
|
|
649
849
|
}
|
|
850
|
+
// 上下文门控(≥70% 时在回合间隙先压缩再发 prompt;等待有界,失败放行)
|
|
851
|
+
await this.contextGate('pre-prompt')
|
|
650
852
|
const queue: AgentEvent[] = []
|
|
651
853
|
let resolveWait: (() => void) | null = null
|
|
652
854
|
let isDone = false
|
|
@@ -929,6 +1131,29 @@ export class OmpRpcAgentImpl implements AgentInterface {
|
|
|
929
1131
|
error: { code: 'OMP_PROCESS_EXIT', message: 'omp 子进程意外退出' },
|
|
930
1132
|
}]
|
|
931
1133
|
|
|
1134
|
+
// ===== 上下文治理事件(回合中到达的原生阈值/overflow 压缩;不污染事件流)=====
|
|
1135
|
+
case 'compaction_start':
|
|
1136
|
+
this.compacting = true
|
|
1137
|
+
return []
|
|
1138
|
+
|
|
1139
|
+
case 'compaction_end': {
|
|
1140
|
+
this.compacting = false
|
|
1141
|
+
const result = (event as { result?: CompactionResult }).result
|
|
1142
|
+
if (result?.summary) {
|
|
1143
|
+
void this.harvestCompaction(
|
|
1144
|
+
result.summary,
|
|
1145
|
+
result.tokensBefore,
|
|
1146
|
+
result.tokensAfter ?? result.estimatedTokensAfter,
|
|
1147
|
+
(event as { reason?: string }).reason,
|
|
1148
|
+
)
|
|
1149
|
+
}
|
|
1150
|
+
return []
|
|
1151
|
+
}
|
|
1152
|
+
|
|
1153
|
+
// 回合完全落定(无自动重试/压缩重试/排队续跑):后续压缩检查由 AgentRuntime 驱动
|
|
1154
|
+
case 'agent_settled':
|
|
1155
|
+
return []
|
|
1156
|
+
|
|
932
1157
|
case '__error__':
|
|
933
1158
|
return [{
|
|
934
1159
|
kind: 'error',
|
|
@@ -1176,7 +1401,12 @@ export class OmpRpcAgentImpl implements AgentInterface {
|
|
|
1176
1401
|
// 只配其一时也显式发送,避免 respawn 后静默回落默认模型)
|
|
1177
1402
|
if (this.config.provider || this.config.model) {
|
|
1178
1403
|
try {
|
|
1179
|
-
|
|
1404
|
+
// 未配置一侧保持 undefined(JSON 序列化丢键)→ omp 以当前已设值补全
|
|
1405
|
+
await client.send({
|
|
1406
|
+
type: 'set_model',
|
|
1407
|
+
provider: this.config.provider as string,
|
|
1408
|
+
modelId: this.config.model as string,
|
|
1409
|
+
})
|
|
1180
1410
|
}
|
|
1181
1411
|
catch {
|
|
1182
1412
|
// 模型设置失败不致命(用 omp 默认模型)
|
|
@@ -1187,11 +1417,30 @@ export class OmpRpcAgentImpl implements AgentInterface {
|
|
|
1187
1417
|
client.onHostToolCall(req => this.handleHostTool(req))
|
|
1188
1418
|
await client.send({ type: 'set_host_tools', tools: hostToolsForRole(this.agentRole) })
|
|
1189
1419
|
|
|
1420
|
+
// 上下文治理探测(feature-detect 一次;失败退化被动 usage 跟踪)+ 原生压缩兜底保持开启
|
|
1421
|
+
void this.probeContext(client).catch(() => {})
|
|
1422
|
+
void client.send({ type: 'set_auto_compaction', enabled: true }).catch(() => {})
|
|
1423
|
+
|
|
1190
1424
|
this.client = client
|
|
1191
1425
|
this.hostToolsRegistered = true
|
|
1192
1426
|
}
|
|
1193
1427
|
}
|
|
1194
1428
|
|
|
1429
|
+
/** 上下文状态探测:get_state 取 contextWindow/sessionId(一次;失败仅损失 percent 精度) */
|
|
1430
|
+
private async probeContext(client: OmpRpcClient): Promise<void> {
|
|
1431
|
+
try {
|
|
1432
|
+
const resp = await client.send({ type: 'get_state' })
|
|
1433
|
+
const data = (resp.data ?? {}) as Record<string, unknown>
|
|
1434
|
+
const model = data.model as { contextWindow?: number } | undefined
|
|
1435
|
+
if (typeof model?.contextWindow === 'number' && model.contextWindow > 0) {
|
|
1436
|
+
this.contextWindow = model.contextWindow
|
|
1437
|
+
client.setContextWindow(model.contextWindow)
|
|
1438
|
+
}
|
|
1439
|
+
if (typeof data.sessionId === 'string' && data.sessionId) this.sessionId = data.sessionId
|
|
1440
|
+
}
|
|
1441
|
+
catch { /* get_state 不可用:仅被动 usage */ }
|
|
1442
|
+
}
|
|
1443
|
+
|
|
1195
1444
|
// ===== 内部:host tool handler(workspace 桥接) =====
|
|
1196
1445
|
|
|
1197
1446
|
private async handleHostTool(req: HostToolCallRequest): Promise<{ text: string, isError?: boolean }> {
|
|
@@ -1309,6 +1558,26 @@ export class OmpRpcAgentImpl implements AgentInterface {
|
|
|
1309
1558
|
return { text: `消息 ${sent.messageId.slice(0, 8)}… 已发送给 ${toAgentId}(priority=${priority})${triggerNote}` }
|
|
1310
1559
|
}
|
|
1311
1560
|
|
|
1561
|
+
case 'send_cross_channel_message': {
|
|
1562
|
+
// 跨 Channel 通信(仅 lead):直投目标 channel 的 lead mailbox
|
|
1563
|
+
const toChannelId = req.arguments.to_channel_id as string
|
|
1564
|
+
const message = req.arguments.message as string
|
|
1565
|
+
if (!toChannelId || !message) return { text: '缺少 to_channel_id 或 message', isError: true }
|
|
1566
|
+
try {
|
|
1567
|
+
const r = await ws.sendCrossChannelMessage({
|
|
1568
|
+
toChannelId,
|
|
1569
|
+
parts: [{ text: message }],
|
|
1570
|
+
requireReply: req.arguments.require_reply === true,
|
|
1571
|
+
inReplyTo: req.arguments.in_reply_to as string | undefined,
|
|
1572
|
+
})
|
|
1573
|
+
const note = req.arguments.require_reply === true ? '(已要求对方 Leader 回复)' : ''
|
|
1574
|
+
return { text: `跨 Channel 消息 ${r.messageId.slice(0, 8)}… 已送达 channel「${r.toChannelName}」的 Leader(${r.toLeadAgentId.slice(0, 8)}…)${note}。对方将按你的信息需求处理;其回复会经 mailbox 到达你这里。` }
|
|
1575
|
+
}
|
|
1576
|
+
catch (err) {
|
|
1577
|
+
return { text: `跨 Channel 发送失败: ${err instanceof Error ? err.message : String(err)}(仅 Leader 可跨 Channel 通信)`, isError: true }
|
|
1578
|
+
}
|
|
1579
|
+
}
|
|
1580
|
+
|
|
1312
1581
|
case 'refuse_task': {
|
|
1313
1582
|
const taskId = req.arguments.task_id as string
|
|
1314
1583
|
const reason = req.arguments.reason as string
|
|
@@ -549,12 +549,27 @@ export interface TeamMemberRow {
|
|
|
549
549
|
createdAt: string
|
|
550
550
|
}
|
|
551
551
|
|
|
552
|
+
/**
|
|
553
|
+
* 记忆类别:episodic-task/episodic-peer/episodic-session/episodic-team-task(harvest 族,
|
|
554
|
+
* 参与过期+淘汰)/ semantic(知识,免衰减)/ brief/chronicle/reflection(策展层,
|
|
555
|
+
* 免向量化免维护)。TEXT 自由列(无 CHECK),联合类型仅约束调用面。
|
|
556
|
+
*/
|
|
557
|
+
export type MemoryKind
|
|
558
|
+
= | 'episodic-task'
|
|
559
|
+
| 'episodic-peer'
|
|
560
|
+
| 'episodic-session'
|
|
561
|
+
| 'episodic-team-task'
|
|
562
|
+
| 'semantic'
|
|
563
|
+
| 'brief'
|
|
564
|
+
| 'chronicle'
|
|
565
|
+
| 'reflection'
|
|
566
|
+
|
|
552
567
|
/** agent_memories 表行(content 为已 CJK 切分存储文本;agentId='__team__' 为团队共享行) */
|
|
553
568
|
export interface MemoryRow {
|
|
554
569
|
id: string
|
|
555
570
|
channelId: string
|
|
556
571
|
agentId: string
|
|
557
|
-
kind:
|
|
572
|
+
kind: MemoryKind
|
|
558
573
|
title: string
|
|
559
574
|
content: string
|
|
560
575
|
importance: number
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
*/
|
|
7
7
|
import { randomUUID } from 'node:crypto'
|
|
8
8
|
import type { DatabaseSync, StatementSync } from 'node:sqlite'
|
|
9
|
-
import type { MemoryRow } from './database'
|
|
9
|
+
import type { MemoryKind, MemoryRow } from './database'
|
|
10
10
|
|
|
11
11
|
/** 团队共享记忆域 sentinel(单 channel 内全员可读) */
|
|
12
12
|
export const TEAM_AGENT_ID = '__team__'
|
|
@@ -14,7 +14,8 @@ export const TEAM_AGENT_ID = '__team__'
|
|
|
14
14
|
export interface MemoryUpsertInput {
|
|
15
15
|
channelId: string
|
|
16
16
|
agentId: string
|
|
17
|
-
|
|
17
|
+
/** 语义见 database.ts MemoryKind(harvest 族参与维护;semantic 知识;策展层双免) */
|
|
18
|
+
kind: MemoryKind
|
|
18
19
|
title: string
|
|
19
20
|
/** title 的 CJK 切分副本(FTS 索引用;调用方经 AgentMemory.segmentCJK 处理,V8) */
|
|
20
21
|
titleFts: string
|
|
@@ -75,6 +76,15 @@ export function createMemoryRepo(db: DatabaseSync) {
|
|
|
75
76
|
const agentIdsStmt = db.prepare(
|
|
76
77
|
`SELECT DISTINCT agent_id FROM agent_memories WHERE agent_id != ?`,
|
|
77
78
|
)
|
|
79
|
+
const channelIdsStmt = db.prepare(
|
|
80
|
+
`SELECT DISTINCT channel_id AS channelId FROM agent_memories`,
|
|
81
|
+
)
|
|
82
|
+
const getByIdStmt = db.prepare(
|
|
83
|
+
`SELECT id, channel_id AS channelId, agent_id AS agentId, kind, title, content,
|
|
84
|
+
importance, task_id AS taskId, access_count AS accessCount,
|
|
85
|
+
last_accessed_at AS lastAccessedAt, created_at AS createdAt
|
|
86
|
+
FROM agent_memories WHERE id = ?`,
|
|
87
|
+
)
|
|
78
88
|
// ===== 向量方法(P1;vec0 分区表 + mem_rowid 辅助列映射)=====
|
|
79
89
|
// 已实测事实:
|
|
80
90
|
// - vec0 分区表不支持显式 rowid 插入 → 以 +mem_rowid 辅助列映射 agent_memories.rowid
|
|
@@ -188,6 +198,16 @@ export function createMemoryRepo(db: DatabaseSync) {
|
|
|
188
198
|
return (agentIdsStmt.all(TEAM_AGENT_ID) as Array<{ agent_id: string }>).map(r => r.agent_id)
|
|
189
199
|
},
|
|
190
200
|
|
|
201
|
+
/** 有记忆的 channel 清单(team 域行按 channel 迭代维护用) */
|
|
202
|
+
listChannelIds(): string[] {
|
|
203
|
+
return (channelIdsStmt.all() as Array<{ channelId: string }>).map(r => r.channelId)
|
|
204
|
+
},
|
|
205
|
+
|
|
206
|
+
/** 按 id 取单行(brief 置顶注入等直取场景;不存在返回 null) */
|
|
207
|
+
getById(id: string): MemoryRow | null {
|
|
208
|
+
return (getByIdStmt.get(id) as MemoryRow | undefined) ?? null
|
|
209
|
+
},
|
|
210
|
+
|
|
191
211
|
// ===== vec:延迟建表(vecInit)/ 刷新写入(vecSet)/ 删除(vecDelete)/ 分区 kNN(vecSearch)=====
|
|
192
212
|
|
|
193
213
|
/** 首次调用按 dims 建表;同 dims 幂等 true;不同 dims 拒绝 false(禁用向量,绝不重建) */
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
import { copyFileSync, existsSync, readdirSync, readFileSync, statSync, mkdirSync } from 'node:fs'
|
|
19
19
|
import { dirname, join, resolve } from 'node:path'
|
|
20
20
|
import { fileURLToPath } from 'node:url'
|
|
21
|
-
import { awHome, findLocalConfigRoot } from '
|
|
21
|
+
import { awHome, findLocalConfigRoot } from '@/shared/config/home.mjs'
|
|
22
22
|
import { AppError } from '../../../utils/errors'
|
|
23
23
|
|
|
24
24
|
/** 模板变量值(字符串化注入) */
|
|
@@ -40,7 +40,8 @@ function packagedPromptsDir(): string | null {
|
|
|
40
40
|
return existsSync(candidate) ? candidate : null
|
|
41
41
|
}
|
|
42
42
|
|
|
43
|
-
/**
|
|
43
|
+
/** 首次/每次解析把包内初始 prompts 补种到 ~/.AgentWorkShop/prompts(逐文件只补缺,不覆盖)。
|
|
44
|
+
* 每次解析都执行:包升级新增的提示词文件能进入已存在的 Home(用户已定制文件不回写)。 */
|
|
44
45
|
function seedHomePrompts(homePrompts: string): void {
|
|
45
46
|
const src = packagedPromptsDir()
|
|
46
47
|
if (!src || resolve(src) === resolve(homePrompts)) return
|
|
@@ -55,7 +56,7 @@ function seedHomePrompts(homePrompts: string): void {
|
|
|
55
56
|
}
|
|
56
57
|
catch { /* 单文件失败不阻断播种 */ }
|
|
57
58
|
}
|
|
58
|
-
if (seeded > 0) console.log(`[prompts] 已向 ${homePrompts}
|
|
59
|
+
if (seeded > 0) console.log(`[prompts] 已向 ${homePrompts} 补种 ${seeded} 个提示词文件`)
|
|
59
60
|
}
|
|
60
61
|
|
|
61
62
|
/**
|
|
@@ -72,9 +73,9 @@ export function promptsDir(): string {
|
|
|
72
73
|
return base
|
|
73
74
|
}
|
|
74
75
|
|
|
75
|
-
// ~/.AgentWorkShop/prompts
|
|
76
|
+
// ~/.AgentWorkShop/prompts:默认兜底目录,解析时自动补种初始文件(只补缺,不覆盖用户定制)
|
|
76
77
|
const homePrompts = join(awHome(process.env), 'prompts')
|
|
77
|
-
|
|
78
|
+
seedHomePrompts(homePrompts)
|
|
78
79
|
|
|
79
80
|
// ② 项目内 ./.AgentWorkShop/prompts 真实存在 → 项目内优先
|
|
80
81
|
const localConfigRoot = findLocalConfigRoot(process.cwd())
|