@openape/ape-agent 2.2.0 → 2.4.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 +90 -29
  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
  };
@@ -4021,14 +4041,14 @@ var PATCH_INTERVAL_MS = 300;
4021
4041
  var ThreadSession = class {
4022
4042
  constructor(deps) {
4023
4043
  this.deps = deps;
4024
- this.resolvedTools = taskTools(deps.tools);
4025
4044
  }
4026
4045
  active;
4027
4046
  queue = [];
4028
4047
  history = [];
4029
- resolvedTools;
4030
- /** No-op placeholder kept for API compatibility with the previous
4031
- * RPC-listener model where dispose() detached the listener. */
4048
+ /**
4049
+ * No-op placeholder kept for API compatibility with the previous
4050
+ * RPC-listener model where dispose() detached the listener.
4051
+ */
4032
4052
  dispose() {
4033
4053
  }
4034
4054
  /** Forward an inbound chat message to the runtime. Queues if a turn is in flight. */
@@ -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,21 +4071,30 @@ 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
+ };
4091
+ const { systemPrompt, tools } = this.deps.resolveConfig();
4062
4092
  try {
4063
4093
  const result = await runLoop({
4064
4094
  config: this.deps.runtimeConfig,
4065
- systemPrompt: this.deps.systemPrompt,
4095
+ systemPrompt,
4066
4096
  userMessage: body,
4067
- tools: this.resolvedTools,
4097
+ tools: taskTools(tools),
4068
4098
  maxSteps: this.deps.maxSteps,
4069
4099
  history: this.history,
4070
4100
  handlers: {
@@ -4075,12 +4105,15 @@ var ThreadSession = class {
4075
4105
  },
4076
4106
  onToolCall: ({ name }) => {
4077
4107
  this.deps.log(`[${this.deps.roomId}/${this.deps.threadId.slice(0, 8)}] tool_call: ${name}`);
4108
+ void setStatus(`\u{1F527} ${name}`);
4078
4109
  },
4079
4110
  onToolResult: ({ name }) => {
4080
4111
  this.deps.log(`[${this.deps.roomId}/${this.deps.threadId.slice(0, 8)}] tool_result: ${name}`);
4112
+ void setStatus(null);
4081
4113
  },
4082
4114
  onToolError: ({ name, error }) => {
4083
4115
  this.deps.log(`[${this.deps.roomId}/${this.deps.threadId.slice(0, 8)}] tool_error: ${name} \u2192 ${error}`);
4116
+ void setStatus(null);
4084
4117
  }
4085
4118
  }
4086
4119
  });
@@ -4091,28 +4124,51 @@ var ThreadSession = class {
4091
4124
  if (result.status === "error") {
4092
4125
  this.deps.log(`runtime done with status=error (room=${this.deps.roomId} thread=${this.deps.threadId})`);
4093
4126
  }
4094
- this.endTurn();
4127
+ await this.endTurn();
4095
4128
  } catch (err) {
4096
4129
  const message = err instanceof Error ? err.message : String(err);
4097
4130
  this.deps.log(`runtime error (room=${this.deps.roomId} thread=${this.deps.threadId}): ${message}`);
4098
- this.failTurn(`(runtime error: ${message})`);
4131
+ await this.failTurn(`(runtime error: ${message})`);
4099
4132
  }
4100
4133
  }
4101
- endTurn() {
4134
+ /**
4135
+ * Stream-end: flush any pending throttled body PATCH, then mark the
4136
+ * message as no-longer-streaming. The combined call also triggers
4137
+ * the user-facing push (the placeholder POST suppressed it).
4138
+ */
4139
+ async endTurn() {
4102
4140
  const turn = this.active;
4103
4141
  if (!turn) return;
4104
4142
  turn.throttle.flush();
4143
+ try {
4144
+ await this.deps.chat.patchMessage(turn.placeholderId, {
4145
+ body: turn.accumulated || "(empty response)",
4146
+ streaming: false,
4147
+ streamingStatus: null
4148
+ });
4149
+ } catch (err) {
4150
+ this.deps.log(`stream-end patch failed: ${err instanceof Error ? err.message : String(err)}`);
4151
+ }
4105
4152
  this.active = void 0;
4106
4153
  const next = this.queue.shift();
4107
4154
  if (next) {
4108
4155
  void this.startTurn(next.body, next.replyToMessageId);
4109
4156
  }
4110
4157
  }
4111
- failTurn(message) {
4158
+ async failTurn(message) {
4112
4159
  const turn = this.active;
4113
4160
  if (!turn) return;
4114
4161
  turn.accumulated = message;
4115
4162
  turn.throttle.flush();
4163
+ try {
4164
+ await this.deps.chat.patchMessage(turn.placeholderId, {
4165
+ body: message,
4166
+ streaming: false,
4167
+ streamingStatus: null
4168
+ });
4169
+ } catch (err) {
4170
+ this.deps.log(`fail-turn patch failed: ${err instanceof Error ? err.message : String(err)}`);
4171
+ }
4116
4172
  this.active = void 0;
4117
4173
  const next = this.queue.shift();
4118
4174
  if (next) {
@@ -4310,16 +4366,21 @@ var Bridge = class {
4310
4366
  threadId,
4311
4367
  chat: this.chat,
4312
4368
  runtimeConfig: this.runtimeConfig(),
4313
- // Tools resolve from agent.json (latest sync from troop) on
4314
- // every new thread, so owner edits in the troop UI take
4315
- // effect after the next sync without a bridge restart.
4316
- // SOUL.md + skills are merged into the system prompt the same
4317
- // way picked up per-thread without restart.
4318
- tools: resolveTools(this.cfg.tools),
4319
- systemPrompt: composeSystemPrompt({
4320
- base: resolveSystemPrompt(this.cfg.systemPrompt),
4321
- enabledTools: resolveTools(this.cfg.tools)
4322
- }),
4369
+ // Resolve tools + systemPrompt on every turn from agent.json
4370
+ // (latest sync from troop). Owner edits in the troop UI thus
4371
+ // take effect on the very next message in an existing thread —
4372
+ // not just on a freshly-opened one. SOUL.md + skills get merged
4373
+ // into the system prompt the same way.
4374
+ resolveConfig: () => {
4375
+ const tools = resolveTools(this.cfg.tools);
4376
+ return {
4377
+ tools,
4378
+ systemPrompt: composeSystemPrompt({
4379
+ base: resolveSystemPrompt(this.cfg.systemPrompt),
4380
+ enabledTools: tools
4381
+ })
4382
+ };
4383
+ },
4323
4384
  maxSteps: this.cfg.maxSteps,
4324
4385
  log
4325
4386
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openape/ape-agent",
3
- "version": "2.2.0",
3
+ "version": "2.4.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",