@openape/ape-agent 2.2.0 → 2.3.0

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.
Files changed (2) hide show
  1. package/dist/bridge.mjs +68 -13
  2. package/package.json +1 -1
package/dist/bridge.mjs CHANGED
@@ -1353,11 +1353,12 @@ var ChatApi = class {
1353
1353
  this.bearer = bearer;
1354
1354
  }
1355
1355
  async postMessage(roomId, body, opts = {}) {
1356
- const trimmed = clamp(body, MAX_BODY);
1356
+ const bodyForServer = opts.streaming ? body : clamp(body, MAX_BODY);
1357
1357
  const url = `${this.endpoint}/api/rooms/${encodeURIComponent(roomId)}/messages`;
1358
- const payload = { body: trimmed };
1358
+ const payload = { body: bodyForServer };
1359
1359
  if (opts.replyTo) payload.reply_to = opts.replyTo;
1360
1360
  if (opts.threadId) payload.thread_id = opts.threadId;
1361
+ if (opts.streaming) payload.streaming = true;
1361
1362
  const result = await ofetch5(url, {
1362
1363
  method: "POST",
1363
1364
  headers: { Authorization: await this.bearer() },
@@ -1401,13 +1402,32 @@ var ChatApi = class {
1401
1402
  body: { name: name.slice(0, 100) }
1402
1403
  });
1403
1404
  }
1404
- async patchMessage(messageId, body) {
1405
- const trimmed = clamp(body, MAX_BODY);
1405
+ /**
1406
+ * Update an in-flight or completed message. The server differentiates
1407
+ * three modes via the message's current `streaming` state and the
1408
+ * `streaming` field in this call:
1409
+ *
1410
+ * - Stream tick: pass `body` only (current accumulated text).
1411
+ * Server keeps streaming=true and does NOT bump edited_at.
1412
+ * - Stream end: pass `body` + `streaming: false`. Server clears
1413
+ * the streaming flag and triggers the user-facing push.
1414
+ * - Tool-call status: pass `streamingStatus` only (no body).
1415
+ * Renders as "🔧 time.now" in the typing-subtitle.
1416
+ * - Tool-call cleared: pass `streamingStatus: null`.
1417
+ */
1418
+ async patchMessage(messageId, opts = {}) {
1406
1419
  const url = `${this.endpoint}/api/messages/${encodeURIComponent(messageId)}`;
1420
+ const payload = {};
1421
+ if (opts.body !== void 0) {
1422
+ payload.body = opts.streaming === false && opts.body.trim().length === 0 ? clamp(opts.body, MAX_BODY) : opts.body.length <= MAX_BODY ? opts.body : `${opts.body.slice(0, MAX_BODY - 1)}\u2026`;
1423
+ }
1424
+ if (opts.streaming !== void 0) payload.streaming = opts.streaming;
1425
+ if (opts.streamingStatus !== void 0) payload.streaming_status = opts.streamingStatus;
1426
+ if (Object.keys(payload).length === 0) return;
1407
1427
  await ofetch5(url, {
1408
1428
  method: "PATCH",
1409
1429
  headers: { Authorization: await this.bearer() },
1410
- body: { body: trimmed }
1430
+ body: payload
1411
1431
  });
1412
1432
  }
1413
1433
  };
@@ -4040,9 +4060,10 @@ var ThreadSession = class {
4040
4060
  void this.startTurn(body, replyToMessageId);
4041
4061
  }
4042
4062
  async startTurn(body, replyToMessageId) {
4043
- const placeholder = await this.deps.chat.postMessage(this.deps.roomId, "\u2026", {
4063
+ const placeholder = await this.deps.chat.postMessage(this.deps.roomId, "", {
4044
4064
  replyTo: replyToMessageId,
4045
- threadId: this.deps.threadId
4065
+ threadId: this.deps.threadId,
4066
+ streaming: true
4046
4067
  });
4047
4068
  const turn = {
4048
4069
  placeholderId: placeholder.id,
@@ -4050,15 +4071,23 @@ var ThreadSession = class {
4050
4071
  replyToMessageId,
4051
4072
  throttle: createThrottle(async () => {
4052
4073
  if (!this.active || this.active.placeholderId !== placeholder.id) return;
4053
- const text = this.active.accumulated || "\u2026";
4074
+ const text = this.active.accumulated;
4075
+ if (text.length === 0) return;
4054
4076
  try {
4055
- await this.deps.chat.patchMessage(placeholder.id, text);
4077
+ await this.deps.chat.patchMessage(placeholder.id, { body: text });
4056
4078
  } catch (err) {
4057
4079
  this.deps.log(`patch failed (room=${this.deps.roomId} thread=${this.deps.threadId}): ${err instanceof Error ? err.message : String(err)}`);
4058
4080
  }
4059
4081
  }, PATCH_INTERVAL_MS)
4060
4082
  };
4061
4083
  this.active = turn;
4084
+ const setStatus = async (status) => {
4085
+ try {
4086
+ await this.deps.chat.patchMessage(placeholder.id, { streamingStatus: status });
4087
+ } catch (err) {
4088
+ this.deps.log(`status patch failed: ${err instanceof Error ? err.message : String(err)}`);
4089
+ }
4090
+ };
4062
4091
  try {
4063
4092
  const result = await runLoop({
4064
4093
  config: this.deps.runtimeConfig,
@@ -4075,12 +4104,15 @@ var ThreadSession = class {
4075
4104
  },
4076
4105
  onToolCall: ({ name }) => {
4077
4106
  this.deps.log(`[${this.deps.roomId}/${this.deps.threadId.slice(0, 8)}] tool_call: ${name}`);
4107
+ void setStatus(`\u{1F527} ${name}`);
4078
4108
  },
4079
4109
  onToolResult: ({ name }) => {
4080
4110
  this.deps.log(`[${this.deps.roomId}/${this.deps.threadId.slice(0, 8)}] tool_result: ${name}`);
4111
+ void setStatus(null);
4081
4112
  },
4082
4113
  onToolError: ({ name, error }) => {
4083
4114
  this.deps.log(`[${this.deps.roomId}/${this.deps.threadId.slice(0, 8)}] tool_error: ${name} \u2192 ${error}`);
4115
+ void setStatus(null);
4084
4116
  }
4085
4117
  }
4086
4118
  });
@@ -4091,28 +4123,51 @@ var ThreadSession = class {
4091
4123
  if (result.status === "error") {
4092
4124
  this.deps.log(`runtime done with status=error (room=${this.deps.roomId} thread=${this.deps.threadId})`);
4093
4125
  }
4094
- this.endTurn();
4126
+ await this.endTurn();
4095
4127
  } catch (err) {
4096
4128
  const message = err instanceof Error ? err.message : String(err);
4097
4129
  this.deps.log(`runtime error (room=${this.deps.roomId} thread=${this.deps.threadId}): ${message}`);
4098
- this.failTurn(`(runtime error: ${message})`);
4130
+ await this.failTurn(`(runtime error: ${message})`);
4099
4131
  }
4100
4132
  }
4101
- endTurn() {
4133
+ /**
4134
+ * Stream-end: flush any pending throttled body PATCH, then mark the
4135
+ * message as no-longer-streaming. The combined call also triggers
4136
+ * the user-facing push (the placeholder POST suppressed it).
4137
+ */
4138
+ async endTurn() {
4102
4139
  const turn = this.active;
4103
4140
  if (!turn) return;
4104
4141
  turn.throttle.flush();
4142
+ try {
4143
+ await this.deps.chat.patchMessage(turn.placeholderId, {
4144
+ body: turn.accumulated || "(empty response)",
4145
+ streaming: false,
4146
+ streamingStatus: null
4147
+ });
4148
+ } catch (err) {
4149
+ this.deps.log(`stream-end patch failed: ${err instanceof Error ? err.message : String(err)}`);
4150
+ }
4105
4151
  this.active = void 0;
4106
4152
  const next = this.queue.shift();
4107
4153
  if (next) {
4108
4154
  void this.startTurn(next.body, next.replyToMessageId);
4109
4155
  }
4110
4156
  }
4111
- failTurn(message) {
4157
+ async failTurn(message) {
4112
4158
  const turn = this.active;
4113
4159
  if (!turn) return;
4114
4160
  turn.accumulated = message;
4115
4161
  turn.throttle.flush();
4162
+ try {
4163
+ await this.deps.chat.patchMessage(turn.placeholderId, {
4164
+ body: message,
4165
+ streaming: false,
4166
+ streamingStatus: null
4167
+ });
4168
+ } catch (err) {
4169
+ this.deps.log(`fail-turn patch failed: ${err instanceof Error ? err.message : String(err)}`);
4170
+ }
4116
4171
  this.active = void 0;
4117
4172
  const next = this.queue.shift();
4118
4173
  if (next) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openape/ape-agent",
3
- "version": "2.2.0",
3
+ "version": "2.3.0",
4
4
  "description": "OpenApe agent runtime: per-agent process that connects to chat.openape.ai, runs the LLM loop with tools + cron tasks, and streams replies back to owners.",
5
5
  "type": "module",
6
6
  "license": "MIT",