Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 8 additions & 12 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -92,16 +92,15 @@ jobs:
sudo apt-get update
sudo apt-get install -y gcc-aarch64-linux-gnu

- name: Build goagent (linux, Debian 11 container)
- name: Build goagent (linux, Ubuntu 20.04 container)
if: matrix.goos == 'linux'
run: |
docker run --rm -v /opt/hostedtoolcache:/opt/hostedtoolcache -v "$PWD":/github/workspace -w /github/workspace debian:11 \
docker run --rm -v /opt/hostedtoolcache:/opt/hostedtoolcache -v "$PWD":/github/workspace -w /github/workspace ubuntu:20.04 \
bash -euxo pipefail -c '
GO_BIN="$(ls -d /opt/hostedtoolcache/go/*/x64/bin | sort -V | tail -1)/go"
export PATH="$(dirname "$GO_BIN"):$PATH"
export GOCACHE="$PWD/go/.gocache" GOMODCACHE="$PWD/go/.gomodcache"
go version
sed -i "s|deb.debian.org|deb.debian.org|g; s|security.debian.org/debian-security|security.debian.org/debian-security|g" /etc/apt/sources.list 2>/dev/null || true
apt-get update
apt-get install -y --no-install-recommends gcc make libc6-dev
mkdir -p dist
Expand Down Expand Up @@ -181,12 +180,11 @@ jobs:
cd rust && cargo build --release --target ${{ matrix.target }}
cp target/${{ matrix.target }}/release/${{ matrix.bin }} ../dist/rustagent-${{ matrix.suffix }}${{ matrix.ext }}

- name: Build rustagent (linux, Debian 11 container)
- name: Build rustagent (linux, Ubuntu 20.04 container)
if: matrix.runner == 'ubuntu-latest'
run: |
docker run --rm -v "$PWD":/github/workspace -w /github/workspace debian:11 \
docker run --rm -v "$PWD":/github/workspace -w /github/workspace ubuntu:20.04 \
bash -euxo pipefail -c '
sed -i "s|deb.debian.org|deb.debian.org|g" /etc/apt/sources.list 2>/dev/null || true
apt-get update
apt-get install -y --no-install-recommends curl ca-certificates build-essential pkg-config
if [ "${{ matrix.target }}" = "aarch64-unknown-linux-gnu" ]; then
Expand Down Expand Up @@ -257,12 +255,11 @@ jobs:
cd webagent && cargo build --release --target ${{ matrix.target }}
cp target/${{ matrix.target }}/release/${{ matrix.bin }} ../dist/webagent-${{ matrix.suffix }}${{ matrix.ext }}

- name: Build webagent (linux, Debian 11 container)
- name: Build webagent (linux, Ubuntu 20.04 container)
if: matrix.runner == 'ubuntu-latest'
run: |
docker run --rm -v "$PWD":/github/workspace -w /github/workspace debian:11 \
docker run --rm -v "$PWD":/github/workspace -w /github/workspace ubuntu:20.04 \
bash -euxo pipefail -c '
sed -i "s|deb.debian.org|deb.debian.org|g" /etc/apt/sources.list 2>/dev/null || true
apt-get update
apt-get install -y --no-install-recommends curl ca-certificates build-essential pkg-config
if [ "${{ matrix.target }}" = "aarch64-unknown-linux-gnu" ]; then
Expand Down Expand Up @@ -313,12 +310,11 @@ jobs:
sudo apt-get update
sudo apt-get install -y ripgrep gcc-aarch64-linux-gnu

- name: Build cagent (linux, Debian 11 container)
- name: Build cagent (linux, Ubuntu 20.04 container)
if: runner.os == 'Linux'
run: |
docker run --rm -v "$PWD":/github/workspace -w /github/workspace debian:11 \
docker run --rm -v "$PWD":/github/workspace -w /github/workspace ubuntu:20.04 \
bash -euxo pipefail -c '
sed -i "s|deb.debian.org|deb.debian.org|g" /etc/apt/sources.list 2>/dev/null || true
apt-get update
apt-get install -y --no-install-recommends gcc make xxd libc6-dev libcurl4-openssl-dev pkg-config
mkdir -p dist
Expand Down
28 changes: 22 additions & 6 deletions c/agent.c
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,9 @@ static void stream_display_callback(void *ctx, const SseEvent *evt) {
if (evt->out_tokens > 0) sctx->accum.out_tokens = evt->out_tokens;
if (evt->cache_read_tokens > 0) sctx->accum.cache_read_tokens = evt->cache_read_tokens;
if (evt->cache_creation_tokens > 0) sctx->accum.cache_creation_tokens = evt->cache_creation_tokens;
sctx->accum.start_ms = evt->start_ms;
sctx->accum.end_ms = evt->end_ms;
sctx->accum.speed_ready = evt->speed_ready;
break;

case SSE_TOOL_CALL_START: {
Expand Down Expand Up @@ -222,6 +225,7 @@ static void stream_display_callback(void *ctx, const SseEvent *evt) {
}
sctx->accum.tool_count = 0;
sctx->accum.stopped = 0;
sctx->accum.speed_ready = 0;
if (sctx->accum.stop_reason) { free(sctx->accum.stop_reason); sctx->accum.stop_reason = NULL; }
if (sctx->accum.error) { free(sctx->accum.error); sctx->accum.error = NULL; }
sctx->accum.in_tokens = 0;
Expand Down Expand Up @@ -1001,6 +1005,8 @@ int agent_loop(Agent *agent, const char *user_input, const char *turn_kind) {
agent->last_cache_creation_tokens = accum->cache_creation_tokens;
agent->last_context_tokens = accum->in_tokens + accum->out_tokens +
accum->cache_read_tokens + accum->cache_creation_tokens;
agent->last_start_ms = accum->start_ms;
agent->last_end_ms = accum->end_ms;

if (accum->in_tokens > 0 || accum->out_tokens > 0) {
DisplayMessage *dm = malloc(sizeof(DisplayMessage));
Expand All @@ -1027,9 +1033,17 @@ int agent_loop(Agent *agent, const char *user_input, const char *turn_kind) {
store_stats_set_int_file(agent->paths.stats, "current_context_tokens",
agent->last_context_tokens);
}
/* 对齐 bash 版 store_stats_update 末尾的 display_term_title */
agent_update_title(agent);
}
/* 仅完整协议终结且 HTTP 成功后的最终 USAGE 才更新速度。
* 独立于正 token 守卫:零用量成功需要清零,失败和中断保留旧值。 */
if (accum->speed_ready && !accum->error && !agent->interrupted) {
long long dur = agent->last_end_ms - agent->last_start_ms;
int speed = (dur > 0) ? (int)((long long)agent->last_output_tokens * 1000LL / dur) : 0;
store_stats_set_int_file(agent->paths.stats, "last_call_speed_tok_per_sec", speed);
}
/* 对齐 bash 版 store_stats_update 末尾的 display_term_title */
if (accum->in_tokens > 0 || accum->out_tokens > 0 || accum->speed_ready)
agent_update_title(agent);

/* ---- 执行工具调用 ---- */
const char **result_ids = NULL;
Expand Down Expand Up @@ -3019,27 +3033,29 @@ void agent_update_title_status(Agent *agent, const char *status) {
int ao = json_get_int(jp.val, "total_output_tokens");
long long ll_cr = json_get_ll(jp.val, "total_cache_read_tokens");
int ct = json_get_int(jp.val, "current_context_tokens");
int speed = json_get_int(jp.val, "last_call_speed_tok_per_sec");

/* 对齐 bash 版 term_title.awk: model T:turn R:req I:in+cr(pct) O:out C:ctx */
/* 对齐 bash 版 term_title.awk: model T:turn R:req I:in+cr(pct) O:out C:ctx S:speedtok/s */
long long total_i = ll_ai + ll_cr;
int pct = (total_i > 0) ? (int)((ll_cr * 100.0 / total_i) + 0.5) : 0;
char pct_s[32];
if (total_i > 0) snprintf(pct_s, sizeof(pct_s), "%d%%", pct);
else snprintf(pct_s, sizeof(pct_s), "—");

char tc_s[32], ar_s[32], total_i_s[32], ao_s[32], ct_s[32];
char tc_s[32], ar_s[32], total_i_s[32], ao_s[32], ct_s[32], speed_s[32];
format_int_commas(tc, tc_s, sizeof(tc_s));
format_int_commas(ar, ar_s, sizeof(ar_s));
format_ll_commas(total_i, total_i_s, sizeof(total_i_s));
format_int_commas(ao, ao_s, sizeof(ao_s));
format_int_commas(ct, ct_s, sizeof(ct_s));
format_int_commas(speed, speed_s, sizeof(speed_s));

int idle = (status && strcmp(status, "idle") == 0 && agent->active_task_count <= 0);
const char *prefix = idle ? "" : "\xe2\x8f\xb3 ";
int progress = idle ? 0 : 3;
FILE *err = agent->err ? agent->err : stderr;
fprintf(err, "\x1b]0;%s%s T:%s R:%s I:%s(%s) O:%s C:%s\x07\x1b]9;4;%d\x07",
prefix, agent->model, tc_s, ar_s, total_i_s, pct_s, ao_s, ct_s, progress);
fprintf(err, "\x1b]0;%s%s T:%s R:%s I:%s(%s) O:%s C:%s S:%stok/s\x07\x1b]9;4;%d\x07",
prefix, agent->model, tc_s, ar_s, total_i_s, pct_s, ao_s, ct_s, speed_s, progress);
fflush(err);

free(stats_content);
Expand Down
2 changes: 2 additions & 0 deletions c/agent.h
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,8 @@ typedef struct {
int last_output_tokens;
int last_cache_read_tokens;
int last_cache_creation_tokens;
long long last_start_ms;
long long last_end_ms;
int active_task_count;
int sub_agent_depth; /* 主代理为 0,第一层子代理为 1 */

Expand Down
12 changes: 8 additions & 4 deletions c/store.c
Original file line number Diff line number Diff line change
Expand Up @@ -152,7 +152,7 @@ int store_session_init(const SessionPaths *p, int is_new) {
"\"total_input_tokens\":0,"
"\"total_output_tokens\":0,\"total_cache_read_tokens\":0,"
"\"total_cache_creation_tokens\":0,\"current_context_tokens\":0,"
"\"last_updated\":\"\"}\n");
"\"last_call_speed_tok_per_sec\":0,\"last_updated\":\"\"}\n");
fclose(f);

/* 写入 session_start 事件(与 bash 版对齐) */
Expand Down Expand Up @@ -543,18 +543,19 @@ static void stats_write_canonical(const char *path,
int total_cache_read_tokens,
int total_cache_creation_tokens,
int current_context_tokens,
int last_call_speed_tok_per_sec,
const char *last_updated) {
StrBuf buf;
sb_init(&buf);
sb_appendf(&buf, "{\"current_turn_count\":%d,\"agent_request_count\":%d,"
"\"compact_request_count\":%d,\"sub_agent_request_count\":%d,"
"\"total_input_tokens\":%d,\"total_output_tokens\":%d,"
"\"total_cache_read_tokens\":%d,\"total_cache_creation_tokens\":%d,"
"\"current_context_tokens\":%d,\"last_updated\":",
"\"current_context_tokens\":%d,\"last_call_speed_tok_per_sec\":%d,\"last_updated\":",
current_turn_count, agent_request_count, compact_request_count,
sub_agent_request_count, total_input_tokens, total_output_tokens,
total_cache_read_tokens, total_cache_creation_tokens,
current_context_tokens);
current_context_tokens, last_call_speed_tok_per_sec);
sb_append_json_string(&buf, last_updated ? last_updated : "");
sb_append(&buf, "}\n");
util_write_file(path, buf.data);
Expand Down Expand Up @@ -617,6 +618,7 @@ void store_stats_set_int_file(const char *path, const char *key, int value) {
int total_input_tokens = 0, total_output_tokens = 0;
int total_cache_read_tokens = 0, total_cache_creation_tokens = 0;
int current_context_tokens = 0;
int last_call_speed_tok_per_sec = 0;

char *content = store_stats_read(path);
if (content && content[0]) {
Expand All @@ -631,6 +633,7 @@ void store_stats_set_int_file(const char *path, const char *key, int value) {
total_cache_read_tokens = json_get_int(jp.val, "total_cache_read_tokens");
total_cache_creation_tokens = json_get_int(jp.val, "total_cache_creation_tokens");
current_context_tokens = json_get_int(jp.val, "current_context_tokens");
last_call_speed_tok_per_sec = json_get_int(jp.val, "last_call_speed_tok_per_sec");
}
}

Expand All @@ -643,6 +646,7 @@ void store_stats_set_int_file(const char *path, const char *key, int value) {
else if (strcmp(key, "total_cache_read_tokens") == 0) total_cache_read_tokens = value;
else if (strcmp(key, "total_cache_creation_tokens") == 0) total_cache_creation_tokens = value;
else if (strcmp(key, "current_context_tokens") == 0) current_context_tokens = value;
else if (strcmp(key, "last_call_speed_tok_per_sec") == 0) last_call_speed_tok_per_sec = value;

time_t now = time(NULL);
struct tm tm_buf;
Expand All @@ -653,7 +657,7 @@ void store_stats_set_int_file(const char *path, const char *key, int value) {
compact_request_count, sub_agent_request_count,
total_input_tokens, total_output_tokens,
total_cache_read_tokens, total_cache_creation_tokens,
current_context_tokens, ts);
current_context_tokens, last_call_speed_tok_per_sec, ts);
if (content) {
free(content);
}
Expand Down
75 changes: 75 additions & 0 deletions c/test_transport.c
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,80 @@ static void check(int condition, const char *label) {
}
}

/* 走实际逐行解析器,分别检查提前 STOP、协议终结和最终 HTTP 门槛。 */
static void feed_stream(StreamCtx *stream, const char *wire) {
check(stream_cb((char *)wire, 1, strlen(wire), stream) == strlen(wire), "流数据完整接收");
}

static void test_speed_completion(void) {
SseAccumulator acc;
sse_accum_init(&acc);
StreamCtx stream = {0};
stream.callback = sse_accum_callback;
stream.ctx = &acc;
stream.provider = "claude";
sb_init(&stream.line_buf);

feed_stream(&stream, "data: {\"type\":\"message_delta\",\"delta\":{\"stop_reason\":\"end_turn\"},\"usage\":{\"output_tokens\":10}}\n\n");
check(acc.stopped && !acc.speed_ready, "提前停止原因不是最终用量");
check(!stream_speed_ready(&stream, CURLE_OK, 200), "Claude 有停止原因但缺少 message_stop 的提前 EOF 保留速度");
feed_stream(&stream, "data: {\"type\":\"message_stop\"}\n\n");
check(stream_speed_ready(&stream, CURLE_OK, 200), "Claude 完整终结且 HTTP 成功允许更新");
check(!acc.speed_ready, "仅协议终结尚未发最终用量时不更新");
check(!stream_speed_ready(&stream, CURLE_PARTIAL_FILE, 200), "协议完整但 HTTP 截断保留速度");
check(!stream_speed_ready(&stream, CURLE_OK, 401), "HTTP 失败保留速度");
volatile int cancelled = 1;
stream.cancelled = &cancelled;
check(!stream_speed_ready(&stream, CURLE_OK, 200), "协议终结后取消仍保留速度");
check(!stream_speed_ready(&stream, CURLE_WRITE_ERROR, 200), "取消引发写入错误保留速度");
emit_simple_event(stream.callback, stream.ctx, SSE_STOP, "interrupted");
check(acc.stopped && !acc.speed_ready, "中断 STOP 不允许更新速度");
stream.cancelled = NULL;

SseEvent final_usage = {0};
final_usage.type = SSE_USAGE;
final_usage.speed_ready = stream_speed_ready(&stream, CURLE_OK, 200);
final_usage.end_ms = 100;
sse_accum_callback(&acc, &final_usage);
check(acc.speed_ready && acc.out_tokens == 10, "最终用量允许更新且保留累计 token");
emit_simple_event(stream.callback, stream.ctx, SSE_RETRY, NULL);
check(!acc.speed_ready && acc.out_tokens == 0, "重试清除速度门槛及用量");
sb_free(&stream.line_buf);

/* 与传输重试一样,每次尝试使用全新的上下文。 */
memset(&stream, 0, sizeof(stream));
stream.callback = sse_accum_callback;
stream.ctx = &acc;
stream.provider = "openai";
sb_init(&stream.line_buf);
check(!stream_speed_ready(&stream, CURLE_OK, 200), "重试不继承协议终结状态");
feed_stream(&stream, "data: {\"object\":\"chat.completion.chunk\",\"choices\":[{\"finish_reason\":\"stop\"}]}\n\n");
check(acc.stopped && !stream_speed_ready(&stream, CURLE_OK, 200), "OpenAI 有 finish_reason 但缺少 DONE 的提前 EOF 保留速度");
feed_stream(&stream, "data: [DONE]\n\n");
check(stream_speed_ready(&stream, CURLE_OK, 200), "OpenAI 完整终结允许更新");
final_usage.speed_ready = stream_speed_ready(&stream, CURLE_OK, 200);
sse_accum_callback(&acc, &final_usage);
check(acc.speed_ready && acc.out_tokens == 0, "零用量成功仍允许清零速度");
stream.protocol_complete = stream.openai_finished = 0;
feed_stream(&stream, "data: [DONE]\n\n");
check(!stream_speed_ready(&stream, CURLE_OK, 200), "仅 DONE 无 finish_reason 不更新");
sb_free(&stream.line_buf);

const char *failed_events[] = {"response.failed", "response.incomplete"};
for (int i = 0; i < 2; i++) {
memset(&stream, 0, sizeof(stream));
stream.callback = sse_accum_callback;
stream.ctx = &acc;
parse_responses_sse_event(&stream, failed_events[i], "{}", 2);
check(stream.responses_terminal && !stream_speed_ready(&stream, CURLE_OK, 200),
"Responses 失败或不完整终结保留速度");
streamctx_free_openai_tools(&stream);
}
sse_accum_free(&acc);
}

int main(void) {
test_speed_completion();
Capture capture = {0};
StreamCtx stream = {0};
stream.callback = capture_event;
Expand All @@ -55,6 +128,7 @@ int main(void) {
parse_responses_sse_event(&stream, "response.function_call_arguments.delta", delta, strlen(delta));
parse_responses_sse_event(&stream, "response.completed", completed, strlen(completed));

check(stream_speed_ready(&stream, CURLE_OK, 200), "Responses completed 允许最终用量更新速度");
check(capture.count == 3, "Responses completed emits tool, usage, stop");
check(capture.events[0].type == SSE_TOOL_CALL && strcmp(capture.events[0].tool_name, "Read") == 0 && strcmp(capture.events[0].tool_id, "call_1") == 0 && strcmp(capture.events[0].tool_input, "{\"path\":\"/tmp/a\"}") == 0, "Responses maps item id and argument delta");
check(capture.events[1].type == SSE_USAGE && capture.events[1].in_tokens == 9 && capture.events[1].out_tokens == 8 && capture.events[1].cache_read_tokens == 6, "Responses prefers nested cached tokens");
Expand All @@ -68,6 +142,7 @@ int main(void) {
stream.ctx = &capture;
const char *error = "{\"reason\":\"upstream failed\"}";
parse_responses_sse_event(&stream, "error", error, strlen(error));
check(!stream_speed_ready(&stream, CURLE_OK, 200), "Responses 错误终结不更新速度");
check(capture.count == 3, "Responses error emits error, usage, stop");
check(capture.events[0].type == SSE_ERROR && strcmp(capture.events[0].content, "upstream failed") == 0, "Responses error uses reason fallback");
check(capture.events[1].type == SSE_USAGE, "Responses error emits usage event");
Expand Down
Loading
Loading