Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| f24e8109a0 | |||
| 725935e270 |
@@ -18,18 +18,17 @@ temperature: 0.2
|
|||||||
8. 每次按需加载技能(skills)前,先明确说明加载理由,并只加载与当前任务直接相关的最小技能集合。默认遵循 **workflow-first**:先查固定工作流 skill,再按需回落到原子 API skills。
|
8. 每次按需加载技能(skills)前,先明确说明加载理由,并只加载与当前任务直接相关的最小技能集合。默认遵循 **workflow-first**:先查固定工作流 skill,再按需回落到原子 API skills。
|
||||||
9. 当 `dynamic_http_call` 返回 `result_mode = referenced` 和 `result_ref` 时,说明当前只拿到了预览;如果后续推理仍需要完整结果,必须调用 `fetch_result_ref` 回读,不能把 preview 当成完整数据。
|
9. 当 `dynamic_http_call` 返回 `result_mode = referenced` 和 `result_ref` 时,说明当前只拿到了预览;如果后续推理仍需要完整结果,必须调用 `fetch_result_ref` 回读,不能把 preview 当成完整数据。
|
||||||
10. 对 `render_ref`、`result_ref` 或其他引用型结果,默认只使用 preview、摘要、局部字段,或直接把引用传给前端工具;如果引用仅用于渲染/展示(例如 `render_junctions`),直接传引用,不要先读取完整内容再重组。
|
10. 对 `render_ref`、`result_ref` 或其他引用型结果,默认只使用 preview、摘要、局部字段,或直接把引用传给前端工具;如果引用仅用于渲染/展示(例如 `render_junctions`),直接传引用,不要先读取完整内容再重组。
|
||||||
11. `render_junctions.render_ref` 必须传持久化引用 ID(`res-...`),严禁传 `/tmp/*.json`、本地文件路径或普通 URL;如果后端只返回文件路径,这个路径不是可用的 render_ref,不能直接传给前端渲染工具。
|
11. 对任何可能很大的引用文件、结果文件或普通大文件,禁止完整读取;优先使用预览、分页、截断、按字段读取、按片段读取或采样读取。只有在没有其他办法且当前推理确实必须依赖完整内容时,才允许读取完整内容,并先明确说明必要性。
|
||||||
12. 对任何可能很大的引用文件、结果文件或普通大文件,禁止完整读取;优先使用预览、分页、截断、按字段读取、按片段读取或采样读取。只有在没有其他办法且当前推理确实必须依赖完整内容时,才允许读取完整内容,并先明确说明必要性。
|
12. 不得通过 sub-agent、并行代理或任何间接方式,去读取引用文件或大文件的完整内容;主 agent 与其调用链中的其他代理都必须遵守同样限制。
|
||||||
13. 不得通过 sub-agent、并行代理或任何间接方式,去读取引用文件或大文件的完整内容;主 agent 与其调用链中的其他代理都必须遵守同样限制。
|
13. 当且仅当出现**长期有效且高价值**的信号时,才允许调用在线学习工具:
|
||||||
14. 当且仅当出现**长期有效且高价值**的信号时,才允许调用在线学习工具:
|
|
||||||
- `memory_manager`:用户明确长期偏好/约束,或当前项目/环境的稳定事实
|
- `memory_manager`:用户明确长期偏好/约束,或当前项目/环境的稳定事实
|
||||||
- `skill_manager`:已经被证明有效且可复用的 workflow / 方法模式;由您自己判断应写入 `.opencode/skills` 树中的哪个 skill 位置
|
- `skill_manager`:已经被证明有效且可复用的 workflow / 方法模式;由您自己判断应写入 `.opencode/skills` 树中的哪个 skill 位置
|
||||||
15. 不要把一次性问题、临时上下文、未经验证的猜测写入任何学习工具。
|
14. 不要把一次性问题、临时上下文、未经验证的猜测写入任何学习工具。
|
||||||
16. 严禁把 token、password、secret、API key、system prompt、隐私数据写入 `memory_manager` 或 `skill_manager`。
|
15. 严禁把 token、password、secret、API key、system prompt、隐私数据写入 `memory_manager` 或 `skill_manager`。
|
||||||
17. 如果内容只是一次性案例、临时纠错或局部证据,当前不要持久化。
|
16. 如果内容只是一次性案例、临时纠错或局部证据,当前不要持久化。
|
||||||
18. 只有在 workflow 经过验证、足够稳定、可被未来同类任务复用时,才调用 `skill_manager`;并优先写入最贴近现有 skill 树语义的位置,中低置信度内容不要落库。
|
17. 只有在 workflow 经过验证、足够稳定、可被未来同类任务复用时,才调用 `skill_manager`;并优先写入最贴近现有 skill 树语义的位置,中低置信度内容不要落库。
|
||||||
19. 在以下任一情况出现时,主动进行一次轻量复盘:连续多轮对话后、完成复杂多工具任务后、用户明确纠正你后、发现了稳定可复用 workflow 后。复盘的目标是判断是否需要沉淀 memory 或 skill,而不是向用户重复总结。
|
18. 在以下任一情况出现时,主动进行一次轻量复盘:连续多轮对话后、完成复杂多工具任务后、用户明确纠正你后、发现了稳定可复用 workflow 后。复盘的目标是判断是否需要沉淀 memory 或 skill,而不是向用户重复总结。
|
||||||
20. 长期知识严格分流:`memory_manager` 仅保存用户长期偏好与稳定 workspace 事实;`skill_manager` 仅保存可复用方法;一次性案例、会话过程与临时结论应优先保留在 session history,需要时使用 `session_search` 检索,不要误写入 memory 或 skill。
|
19. 长期知识严格分流:`memory_manager` 仅保存用户长期偏好与稳定 workspace 事实;`skill_manager` 仅保存可复用方法;一次性案例、会话过程与临时结论应优先保留在 session history,需要时使用 `session_search` 检索,不要误写入 memory 或 skill。
|
||||||
21. 写入 `memory_manager` 时,将内容写成简短陈述事实,不要写成命令句、提醒句或流程步骤。
|
20. 写入 `memory_manager` 时,将内容写成简短陈述事实,不要写成命令句、提醒句或流程步骤。
|
||||||
22. 更新 skill 时,优先补充现有 skill 的 `Learned Patterns`、`references/` 或 `scripts/`;可复用脚本仅允许写到当前 skill 自己的 `scripts/*.py`,不要放到 `data/` 或其他 skill 目录。
|
21. 更新 skill 时,优先补充现有 skill 的 `Learned Patterns`、`references/` 或 `scripts/`;可复用脚本仅允许写到当前 skill 自己的 `scripts/*.py`,不要放到 `data/` 或其他 skill 目录。
|
||||||
23. 当用户问题依赖过去会话中的案例、约束、决策或相似问题时,优先调用 `session_search`,避免让用户重复描述,也避免把历史案例误当成长期 memory。
|
22. 当用户问题依赖过去会话中的案例、约束、决策或相似问题时,优先调用 `session_search`,避免让用户重复描述,也避免把历史案例误当成长期 memory。
|
||||||
|
|||||||
@@ -1,7 +1,5 @@
|
|||||||
import { tool } from "@opencode-ai/plugin";
|
import { tool } from "@opencode-ai/plugin";
|
||||||
|
|
||||||
const persistentRenderRefPattern = /^res-[a-z0-9-]+$/i;
|
|
||||||
|
|
||||||
export default tool({
|
export default tool({
|
||||||
description:
|
description:
|
||||||
"在前端地图上对 junctions 图层应用分区渲染。优先直接传入 render_ref(指向已持久化的渲染结果引用,格式应为 res-...),不要传 /tmp/*.json 之类的临时文件路径,也不要先把 ref 内容完整读出再重组;前端会自行根据 render_ref 拉取完整 payload 并渲染,这样可以避免 LLM 读取大型 node_area_map。若必须自行构造供 render_ref 引用的 JSON,其 data 结构必须为 { node_area_map: Record<string, string>, area_ids?: string[], area_colors?: Record<string, string> },其中 node_area_map 的 key 是 junction/node id,value 是 area id。",
|
"在前端地图上对 junctions 图层应用分区渲染。优先直接传入 render_ref(指向已持久化的渲染结果引用,格式应为 res-...),不要传 /tmp/*.json 之类的临时文件路径,也不要先把 ref 内容完整读出再重组;前端会自行根据 render_ref 拉取完整 payload 并渲染,这样可以避免 LLM 读取大型 node_area_map。若必须自行构造供 render_ref 引用的 JSON,其 data 结构必须为 { node_area_map: Record<string, string>, area_ids?: string[], area_colors?: Record<string, string> },其中 node_area_map 的 key 是 junction/node id,value 是 area id。",
|
||||||
@@ -11,11 +9,6 @@ export default tool({
|
|||||||
.describe("Why this junction rendering action is needed for the user request."),
|
.describe("Why this junction rendering action is needed for the user request."),
|
||||||
render_ref: tool.schema
|
render_ref: tool.schema
|
||||||
.string()
|
.string()
|
||||||
.trim()
|
|
||||||
.regex(
|
|
||||||
persistentRenderRefPattern,
|
|
||||||
"render_ref 必须是持久化结果引用(例如 res-1234abcd),不能是 /tmp/*.json 文件路径",
|
|
||||||
)
|
|
||||||
.describe(
|
.describe(
|
||||||
"渲染引用 ID。必须是持久化结果引用(res-...),不要传 /tmp/*.json 或其他本地路径。前端会按该引用读取完整 payload.data 并渲染,不需要先用 fetch_result_ref 提取完整数据。render_ref 对应的数据结构必须是 { node_area_map: { [junctionId]: areaId }, area_ids?: string[], area_colors?: { [areaId]: color } };node_area_map 必填,area_ids / area_colors 可选。",
|
"渲染引用 ID。必须是持久化结果引用(res-...),不要传 /tmp/*.json 或其他本地路径。前端会按该引用读取完整 payload.data 并渲染,不需要先用 fetch_result_ref 提取完整数据。render_ref 对应的数据结构必须是 { node_area_map: { [junctionId]: areaId }, area_ids?: string[], area_colors?: { [areaId]: color } };node_area_map 必填,area_ids / area_colors 可选。",
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -20,8 +20,6 @@ import {
|
|||||||
type SupportedModel,
|
type SupportedModel,
|
||||||
} from "./chatStream.js";
|
} from "./chatStream.js";
|
||||||
|
|
||||||
const persistentRenderRefPattern = /^res-[a-z0-9-]+$/i;
|
|
||||||
|
|
||||||
const payloadSchema = z.object({
|
const payloadSchema = z.object({
|
||||||
message: z.string().min(1).max(10000),
|
message: z.string().min(1).max(10000),
|
||||||
session_id: z.string().max(128).optional(),
|
session_id: z.string().max(128).optional(),
|
||||||
@@ -69,13 +67,6 @@ export const buildChatRouter = (
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!persistentRenderRefPattern.test(renderRef)) {
|
|
||||||
res.status(400).json({
|
|
||||||
message: "render_ref must be a persistent ref like res-..., not a file path",
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const result = await resultReferenceStore.getFullAuthorized(renderRef, {
|
const result = await resultReferenceStore.getFullAuthorized(renderRef, {
|
||||||
actorKey: toActorKey(userId),
|
actorKey: toActorKey(userId),
|
||||||
clientSessionId,
|
clientSessionId,
|
||||||
|
|||||||
@@ -318,6 +318,11 @@ export const streamPromptResponse = async ({
|
|||||||
const reasoningDeltas = new Map<string, string[]>();
|
const reasoningDeltas = new Map<string, string[]>();
|
||||||
const reasoningStatuses = new Map<string, "running" | "completed">();
|
const reasoningStatuses = new Map<string, "running" | "completed">();
|
||||||
const toolStatuses = new Map<string, string>();
|
const toolStatuses = new Map<string, string>();
|
||||||
|
let firstSessionEventLogged = false;
|
||||||
|
let firstNonStatusEventLogged = false;
|
||||||
|
let firstTokenLogged = false;
|
||||||
|
let firstReasoningLogged = false;
|
||||||
|
let firstToolEventLogged = false;
|
||||||
let lastSessionStatus: string | null = null;
|
let lastSessionStatus: string | null = null;
|
||||||
let lastSessionStatusMessage: string | null = null;
|
let lastSessionStatusMessage: string | null = null;
|
||||||
let emittedText = false;
|
let emittedText = false;
|
||||||
@@ -470,6 +475,16 @@ export const streamPromptResponse = async ({
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!firstSessionEventLogged) {
|
||||||
|
firstSessionEventLogged = true;
|
||||||
|
logDevelopmentDebug("first session event received", {
|
||||||
|
...debugContext,
|
||||||
|
eventType: event.type,
|
||||||
|
elapsedMs: Math.max(0, Date.now() - requestStartedAt),
|
||||||
|
sincePromptDispatchMs: Math.max(0, Date.now() - promptStartedAt),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
if (event.type === "session.status") {
|
if (event.type === "session.status") {
|
||||||
const nextStatus = event.properties.status.type;
|
const nextStatus = event.properties.status.type;
|
||||||
const nextStatusMessage =
|
const nextStatusMessage =
|
||||||
@@ -505,6 +520,16 @@ export const streamPromptResponse = async ({
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!firstNonStatusEventLogged) {
|
||||||
|
firstNonStatusEventLogged = true;
|
||||||
|
logDevelopmentDebug("first non-status session event received", {
|
||||||
|
...debugContext,
|
||||||
|
eventType: event.type,
|
||||||
|
elapsedMs: Math.max(0, Date.now() - requestStartedAt),
|
||||||
|
sincePromptDispatchMs: Math.max(0, Date.now() - promptStartedAt),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
if (isSkillEvent(event)) {
|
if (isSkillEvent(event)) {
|
||||||
const { name, reason, payload } = extractSkillAuditInfo(event);
|
const { name, reason, payload } = extractSkillAuditInfo(event);
|
||||||
logDevelopmentDebug("skill event received", {
|
logDevelopmentDebug("skill event received", {
|
||||||
@@ -532,12 +557,30 @@ export const streamPromptResponse = async ({
|
|||||||
if (event.type === "message.part.delta" && event.properties.field === "text") {
|
if (event.type === "message.part.delta" && event.properties.field === "text") {
|
||||||
const partType = partTypes.get(event.properties.partID);
|
const partType = partTypes.get(event.properties.partID);
|
||||||
if (partType === "text") {
|
if (partType === "text") {
|
||||||
|
if (!firstTokenLogged) {
|
||||||
|
firstTokenLogged = true;
|
||||||
|
logDevelopmentDebug("first response token emitted", {
|
||||||
|
...debugContext,
|
||||||
|
partId: event.properties.partID,
|
||||||
|
elapsedMs: Math.max(0, Date.now() - requestStartedAt),
|
||||||
|
sincePromptDispatchMs: Math.max(0, Date.now() - promptStartedAt),
|
||||||
|
});
|
||||||
|
}
|
||||||
emittedText = true;
|
emittedText = true;
|
||||||
write("token", {
|
write("token", {
|
||||||
session_id: clientSessionId,
|
session_id: clientSessionId,
|
||||||
content: event.properties.delta,
|
content: event.properties.delta,
|
||||||
});
|
});
|
||||||
} else if (partType === "reasoning") {
|
} else if (partType === "reasoning") {
|
||||||
|
if (!firstReasoningLogged) {
|
||||||
|
firstReasoningLogged = true;
|
||||||
|
logDevelopmentDebug("first reasoning delta received", {
|
||||||
|
...debugContext,
|
||||||
|
partId: event.properties.partID,
|
||||||
|
elapsedMs: Math.max(0, Date.now() - requestStartedAt),
|
||||||
|
sincePromptDispatchMs: Math.max(0, Date.now() - promptStartedAt),
|
||||||
|
});
|
||||||
|
}
|
||||||
const pending = reasoningDeltas.get(event.properties.partID) ?? [];
|
const pending = reasoningDeltas.get(event.properties.partID) ?? [];
|
||||||
pending.push(event.properties.delta);
|
pending.push(event.properties.delta);
|
||||||
reasoningDeltas.set(event.properties.partID, pending);
|
reasoningDeltas.set(event.properties.partID, pending);
|
||||||
@@ -593,6 +636,17 @@ export const streamPromptResponse = async ({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
if (part.type === "tool") {
|
if (part.type === "tool") {
|
||||||
|
if (!firstToolEventLogged) {
|
||||||
|
firstToolEventLogged = true;
|
||||||
|
logDevelopmentDebug("first tool event received", {
|
||||||
|
...debugContext,
|
||||||
|
partId: part.id,
|
||||||
|
tool: part.tool,
|
||||||
|
status: part.state.status,
|
||||||
|
elapsedMs: Math.max(0, Date.now() - requestStartedAt),
|
||||||
|
sincePromptDispatchMs: Math.max(0, Date.now() - promptStartedAt),
|
||||||
|
});
|
||||||
|
}
|
||||||
const toolParams = normalizeToolParams(part.state.input);
|
const toolParams = normalizeToolParams(part.state.input);
|
||||||
const reason = extractRequestReason(toolParams);
|
const reason = extractRequestReason(toolParams);
|
||||||
const isToolFinalState =
|
const isToolFinalState =
|
||||||
|
|||||||
@@ -7,6 +7,18 @@ import {
|
|||||||
import { config } from "../config.js";
|
import { config } from "../config.js";
|
||||||
import { logger } from "../logger.js";
|
import { logger } from "../logger.js";
|
||||||
|
|
||||||
|
const isDevelopmentDebugLoggingEnabled = process.env.NODE_ENV === "development";
|
||||||
|
|
||||||
|
const logDevelopmentDebug = (
|
||||||
|
message: string,
|
||||||
|
metadata: Record<string, unknown>,
|
||||||
|
) => {
|
||||||
|
if (!isDevelopmentDebugLoggingEnabled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
logger.info(metadata, message);
|
||||||
|
};
|
||||||
|
|
||||||
export type RuntimeHealth = {
|
export type RuntimeHealth = {
|
||||||
healthy: boolean;
|
healthy: boolean;
|
||||||
version: string;
|
version: string;
|
||||||
@@ -59,11 +71,27 @@ export class OpencodeRuntimeAdapter {
|
|||||||
|
|
||||||
async prompt(sessionId: string, text: string, model?: RuntimeModelOverride) {
|
async prompt(sessionId: string, text: string, model?: RuntimeModelOverride) {
|
||||||
const client = await this.ensureClient();
|
const client = await this.ensureClient();
|
||||||
|
const startedAt = Date.now();
|
||||||
|
logDevelopmentDebug(
|
||||||
|
"dispatching opencode session.prompt",
|
||||||
|
{
|
||||||
|
sessionId,
|
||||||
|
model: model ?? null,
|
||||||
|
textChars: text.length,
|
||||||
|
},
|
||||||
|
);
|
||||||
await client.session.prompt({
|
await client.session.prompt({
|
||||||
sessionID: sessionId,
|
sessionID: sessionId,
|
||||||
model,
|
model,
|
||||||
parts: [{ type: "text", text }],
|
parts: [{ type: "text", text }],
|
||||||
});
|
});
|
||||||
|
logDevelopmentDebug(
|
||||||
|
"opencode session.prompt returned",
|
||||||
|
{
|
||||||
|
sessionId,
|
||||||
|
elapsedMs: Math.max(0, Date.now() - startedAt),
|
||||||
|
},
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
async messages(sessionId: string, limit = 20) {
|
async messages(sessionId: string, limit = 20) {
|
||||||
|
|||||||
Reference in New Issue
Block a user