Initialize repository with harness code and assets
Initial import of all source code, config, and README assets: the packages workspace (cli, core, server, web, docs, landing, skills), build scripts, tooling config, and CI workflows. Includes the data-layout revision made on this branch: the local data root defaults to ~/.penguin/data (PENGUIN_HOME still overrides; the installer keeps its binaries in ~/.penguin), and every Agent lives under <project>/agents/<agent>/ — path helpers, the three agent-enumeration scans, the system prompt, built-in Skills, tests and docs all follow the new layout. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018ihk8iQuo3kv2aPjAYEPuR
This commit is contained in:
1 parent
056bed7aeb
commit
45bfae6e94
543 files changed
+92949
No files matched your search
@@ -0,0 +1,235 @@
|
||||
/**
|
||||
* Mock Anthropic Messages API (streaming SSE) for E2E.
|
||||
* Branches on request body:
|
||||
* - title request (prompt contains "concise title") -> short text
|
||||
* - files-card probe ("文件卡测试") -> text with two backtick paths (one real, one missing)
|
||||
* - subagent's own turn (its prompt is the only user text) -> final text
|
||||
* - parent asked to delegate ("跑子agent") -> tool_use(run_subagent)
|
||||
* - last message has tool_result -> final text (turn 2)
|
||||
* - otherwise (first user turn) -> thinking + text + tool_use(exec_command)
|
||||
*/
|
||||
import http from "node:http";
|
||||
|
||||
/** The run_subagent prompt; also the marker the mock uses to detect "this is the child session's own request". */
|
||||
const SUBAGENT_PROMPT = "统计仓库里的 TODO 数量";
|
||||
|
||||
const PORT = Number(process.env.MOCK_PORT || 8931);
|
||||
|
||||
/** Count of non-replay requests seen in the "bad stream" conversation: the 1st is cut off (malformed), later ones are retries that get a full tool call. */
|
||||
let malformedTurns = 0;
|
||||
|
||||
function sse(res, event, data) {
|
||||
res.write(`event: ${event}\n`);
|
||||
res.write(`data: ${JSON.stringify(data)}\n\n`);
|
||||
}
|
||||
|
||||
function messageStart(res, msgCount = 1) {
|
||||
sse(res, "message_start", {
|
||||
type: "message_start",
|
||||
message: {
|
||||
id: "msg_mock",
|
||||
type: "message",
|
||||
role: "assistant",
|
||||
model: "claude-4-8",
|
||||
content: [],
|
||||
stop_reason: null,
|
||||
stop_sequence: null,
|
||||
// Usage grows with context length (as real providers do): more messages in the request
|
||||
// means a larger prompt. If the mock reported a fixed value, the compaction threshold
|
||||
// would either be hit on the very first request or never be hit at all, and it wouldn't
|
||||
// drop back down after compaction — this would make it impossible to reproduce the
|
||||
// "compaction only triggers at round end" timing, and would send the engine into an
|
||||
// infinite compaction loop.
|
||||
usage: {
|
||||
input_tokens: 40,
|
||||
output_tokens: 0,
|
||||
cache_read_input_tokens: 40 * msgCount,
|
||||
cache_creation_input_tokens: 10,
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
function messageStop(res, stopReason, outputTokens) {
|
||||
sse(res, "message_delta", {
|
||||
type: "message_delta",
|
||||
delta: { stop_reason: stopReason, stop_sequence: null },
|
||||
usage: { output_tokens: outputTokens },
|
||||
});
|
||||
sse(res, "message_stop", { type: "message_stop" });
|
||||
res.end();
|
||||
}
|
||||
|
||||
function block(res, index, start, deltas, extra) {
|
||||
sse(res, "content_block_start", { type: "content_block_start", index, content_block: start });
|
||||
for (const d of deltas)
|
||||
sse(res, "content_block_delta", { type: "content_block_delta", index, delta: d });
|
||||
if (extra) sse(res, "content_block_delta", { type: "content_block_delta", index, delta: extra });
|
||||
sse(res, "content_block_stop", { type: "content_block_stop", index });
|
||||
}
|
||||
|
||||
const server = http.createServer((req, res) => {
|
||||
if (req.method !== "POST") {
|
||||
res.writeHead(404).end();
|
||||
return;
|
||||
}
|
||||
let body = "";
|
||||
req.on("data", (c) => (body += c));
|
||||
req.on("end", () => {
|
||||
let json = {};
|
||||
try {
|
||||
json = JSON.parse(body);
|
||||
} catch {}
|
||||
const messages = json.messages || [];
|
||||
const flat = JSON.stringify(messages);
|
||||
const isTitle = flat.includes("concise title");
|
||||
// After compaction the new context has only the summary left, so the message count drops
|
||||
// sharply -> reported usage drops along with it, letting compaction converge.
|
||||
const msgCount = messages.length;
|
||||
const hasToolResult = flat.includes("tool_result");
|
||||
// Child-session request: the context has only the prompt handed down by run_subagent, no parent user input.
|
||||
const isSubagentTurn = flat.includes(SUBAGENT_PROMPT) && !flat.includes("跑子agent");
|
||||
const wantsSubagent = flat.includes("跑子agent");
|
||||
// "Bad stream" test case: the first request streams half the tool_use arguments then cuts
|
||||
// the connection (no message_stop), so AgentHub reports "stream incomplete" -> GenerativeModel
|
||||
// resolves it as malformed. On reconnect the engine **resends the input verbatim** — in this
|
||||
// scenario the failed attempt only has a half tool_call (never committed to the ledger), so
|
||||
// the retry request carries no <turn_retried> block and is byte-for-byte identical to the
|
||||
// first request; the mock can only tell them apart by request count (see the malformedTurns counter).
|
||||
const wantsMalformed = flat.includes("坏流测试");
|
||||
|
||||
res.writeHead(200, {
|
||||
"content-type": "text/event-stream",
|
||||
"cache-control": "no-cache",
|
||||
connection: "keep-alive",
|
||||
});
|
||||
messageStart(res, msgCount);
|
||||
|
||||
if (isTitle) {
|
||||
// The child session's title request carries **the child session's own answer** as
|
||||
// assistant material; respond with a distinguishable title based on that, so the E2E
|
||||
// test can prove the child session's title really comes from its own conversation,
|
||||
// not from the run_subagent prompt.
|
||||
const forSubagent = flat.includes("子 agent 报告");
|
||||
block(res, 0, { type: "text", text: "" }, [
|
||||
{ type: "text_delta", text: forSubagent ? "子 agent 的 TODO 统计" : "配置 Tailwind 主题" },
|
||||
]);
|
||||
messageStop(res, "end_turn", 8);
|
||||
return;
|
||||
}
|
||||
|
||||
// Files-card test case: the reply carries two backtick paths — demo.html was actually
|
||||
// written into the Workspace beforehand by the spec via files/content, while
|
||||
// missing-report.pdf doesn't exist; the card should only list the former. This branch
|
||||
// must be checked before hasToolResult (the same session's history already has a
|
||||
// first-round tool_result).
|
||||
if (flat.includes("文件卡测试")) {
|
||||
block(res, 0, { type: "text", text: "" }, [
|
||||
{
|
||||
type: "text_delta",
|
||||
text: "报告已生成:`demo.html`;另一个文件 `missing-report.pdf` 并不存在。",
|
||||
},
|
||||
]);
|
||||
messageStop(res, "end_turn", 18);
|
||||
return;
|
||||
}
|
||||
|
||||
if (wantsMalformed && !hasToolResult) {
|
||||
malformedTurns += 1;
|
||||
if (malformedTurns === 1) {
|
||||
sse(res, "content_block_start", {
|
||||
type: "content_block_start",
|
||||
index: 0,
|
||||
content_block: {
|
||||
type: "tool_use",
|
||||
id: "toolu_broken_1",
|
||||
name: "exec_command",
|
||||
input: {},
|
||||
},
|
||||
});
|
||||
sse(res, "content_block_delta", {
|
||||
type: "content_block_delta",
|
||||
index: 0,
|
||||
delta: { type: "input_json_delta", partial_json: '{"cmd": "ec' },
|
||||
});
|
||||
sse(res, "content_block_stop", { type: "content_block_stop", index: 0 });
|
||||
res.end(); // ends normally but is missing message_delta/message_stop -> AgentHub reports "stream incomplete"
|
||||
return;
|
||||
}
|
||||
// Retry (original input resent): return a complete tool_use, then proceed normally.
|
||||
block(res, 0, { type: "tool_use", id: "toolu_retry_1", name: "exec_command", input: {} }, [
|
||||
{ type: "input_json_delta", partial_json: '{"cmd"' },
|
||||
{ type: "input_json_delta", partial_json: ': "echo ok"}' },
|
||||
]);
|
||||
messageStop(res, "tool_use", 12);
|
||||
return;
|
||||
}
|
||||
|
||||
if (isSubagentTurn) {
|
||||
block(res, 0, { type: "text", text: "" }, [
|
||||
{ type: "text_delta", text: "子 agent 报告:3 处 TODO" },
|
||||
]);
|
||||
messageStop(res, "end_turn", 12);
|
||||
return;
|
||||
}
|
||||
|
||||
if (wantsSubagent && !hasToolResult) {
|
||||
block(res, 0, { type: "tool_use", id: "toolu_mock_sub", name: "run_subagent", input: {} }, [
|
||||
{ type: "input_json_delta", partial_json: '{"prompt": ' },
|
||||
{ type: "input_json_delta", partial_json: `${JSON.stringify(SUBAGENT_PROMPT)}}` },
|
||||
]);
|
||||
messageStop(res, "tool_use", 15);
|
||||
return;
|
||||
}
|
||||
|
||||
if (hasToolResult) {
|
||||
// Turn 2: final answer text.
|
||||
block(res, 0, { type: "text", text: "" }, [
|
||||
{ type: "text_delta", text: "命令已执行完成," },
|
||||
{ type: "text_delta", text: "结果符合预期。" },
|
||||
]);
|
||||
messageStop(res, "end_turn", 20);
|
||||
return;
|
||||
}
|
||||
|
||||
// Turn 1: thinking + text + tool_use, streamed with small delays.
|
||||
const steps = [];
|
||||
steps.push(() =>
|
||||
block(
|
||||
res,
|
||||
0,
|
||||
{ type: "thinking", thinking: "" },
|
||||
[
|
||||
{ type: "thinking_delta", thinking: "先看看" },
|
||||
{ type: "thinking_delta", thinking: "目录结构" },
|
||||
],
|
||||
{ type: "signature_delta", signature: "sig_mock_abc" },
|
||||
),
|
||||
);
|
||||
steps.push(() =>
|
||||
block(res, 1, { type: "text", text: "" }, [
|
||||
{ type: "text_delta", text: "我来运行一个命令查看。" },
|
||||
]),
|
||||
);
|
||||
steps.push(() =>
|
||||
block(res, 2, { type: "tool_use", id: "toolu_mock_1", name: "exec_command", input: {} }, [
|
||||
{ type: "input_json_delta", partial_json: '{"cmd"' },
|
||||
{ type: "input_json_delta", partial_json: ': "ls -la"}' },
|
||||
]),
|
||||
);
|
||||
let i = 0;
|
||||
const run = () => {
|
||||
if (i < steps.length) {
|
||||
steps[i]();
|
||||
i += 1;
|
||||
setTimeout(run, 120);
|
||||
} else {
|
||||
messageStop(res, "tool_use", 30);
|
||||
}
|
||||
};
|
||||
run();
|
||||
});
|
||||
});
|
||||
|
||||
server.listen(PORT, "127.0.0.1", () => {
|
||||
console.log(`mock-llm on http://127.0.0.1:${PORT}`);
|
||||
});
|
||||
Reference in new issue
Block a user