@economic/agents 3.1.0 → 3.2.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.
package/README.md CHANGED
@@ -675,6 +675,24 @@ export class SupportAgent extends Agent<RequestContext, UserContext> {
675
675
  - **Requests without the caller's token,** such as an OAuth callback or a signed webhook, are refused unless `acceptsWithoutToken(request)` returns `true` for them. Check them another way.
676
676
  - **Any authenticator works:** `getAuthenticator(env)` can return your own `Authenticator`, such as one for a different identity provider.
677
677
 
678
+ ### Reading without chatting
679
+
680
+ Return `false` from `canChat(userContext)` to let a user read their chats but not chat in them.
681
+
682
+ - **Refused:** messages, approval answers, a client tool's result that would start another turn, and `run`. Nothing they send is kept.
683
+ - **Still allowed:** reading the chat, rating messages, stopping a turn, and a client tool's result for the turn that's running. A running turn finishes.
684
+ - **The client** is told in the `connected` frame (`canChat: false`), which `useChat` returns as `canChat`.
685
+ - **When it's asked:** once per connection, when the user connects, so a change reaches them when they reconnect. An error counts as `false`. An override of `onConnect` must call `super.onConnect`.
686
+ - **The connection** is marked readonly, so it can't change the agent's state either.
687
+
688
+ ```ts
689
+ export class SupportChat extends ChatAgent<RequestContext, UserContext> {
690
+ canChat(userContext: UserContext | undefined) {
691
+ return userContext?.hasSupportAgent === true;
692
+ }
693
+ }
694
+ ```
695
+
678
696
  ## Memory
679
697
 
680
698
  Return `memory: true` from `getAgentConfig()` to give each user a private memory with the agent, kept across all their chats in their `UserStore` (`AGENTS_USERS`). It's a writable block of the system prompt, which the model updates itself with `set_context`.
@@ -792,6 +810,7 @@ Imported from `@economic/agents` unless noted.
792
810
  | `configureContext()` | `Agent` | Think's system prompt blocks. An override must include `...(await super.configureContext())`, or the agent's own prompt and rules are lost. |
793
811
  | `getTracingHandlers()` | `Agent` | Destinations for completed turns and message feedback (default: `[]`). |
794
812
  | `static getAuthenticator(env)` | `Agent` | Optional caller authentication, credentials and user context. |
813
+ | `canChat(userContext)` | `Agent` | Whether the user can send messages and answer approvals (default `true`). See [Reading without chatting](#reading-without-chatting). |
795
814
  | `submitMessageFeedback` / `getMessageFeedback` | `ChatAgent` | Callable message feedback. |
796
815
  | `getAgentConfig(ctx?)` | `Agent` | Settings for a turn (default `{}`): `maxSteps`, `temperature`, `maxOutputTokens`, `topP`, `toolChoice`, `maxRetries`, `timeout`, `sendReasoning`, `providerOptions`; `toolCallLimits`, how often each user may call each tool by name, per day (`dailyCalls`) and per minute (`burst`), which needs `AGENTS_USERS`; for a chat, `memory`, a private memory per user, and `deleteMessagesAfterMs`, after which its messages are deleted. |
797
816
  | `agent` | `Assistant` | Required. Your `ChatAgent` subclass. |
package/dist/index.d.mts CHANGED
@@ -12873,6 +12873,24 @@ export declare abstract class Agent<RequestContext extends Record<string, unknow
12873
12873
  */
12874
12874
  protected acceptsWithoutToken(_request: Request): boolean;
12875
12875
  onConnect(connection: Connection, ctx: ConnectionContext): Promise<void>;
12876
+ /**
12877
+ * Whether the user can send messages and answer approvals. When it's
12878
+ * `false` they can still read the chat. Asked for each connection in
12879
+ * `onConnect`, so an override of `onConnect` must call `super`. An error
12880
+ * counts as `false`. `true` by default.
12881
+ */
12882
+ canChat(_userContext: UserContext | undefined): boolean | Promise<boolean>;
12883
+ /** Answers still being asked, by connection, so messages sent meanwhile wait for them. */
12884
+ private _canChatAnswers;
12885
+ /**
12886
+ * Asks `canChat` for a connection. A no marks it readonly, which outlives
12887
+ * hibernation and the user's token.
12888
+ */
12889
+ private _askCanChat;
12890
+ /** Whether the connection can chat. Calls from the server, without one, can. */
12891
+ protected _canChat(connection?: Connection | undefined): Promise<boolean>;
12892
+ /** Think stores a message before any hook runs, so it's refused before Think sees it. */
12893
+ private _refuseChatMessages;
12876
12894
  protected _turn: CurrentTurnState<RequestContext, UserContext>;
12877
12895
  protected createToolContextForCurrentTurn(requestContext?: RequestContext): ToolContext<RequestContext, UserContext>;
12878
12896
  protected getToolContextForCurrentTurn(): ToolContext<RequestContext, UserContext> | undefined;
package/dist/index.mjs CHANGED
@@ -71011,11 +71011,12 @@ var ToolApprovals = class {
71011
71011
  }
71012
71012
  };
71013
71013
  // src/server/util/agent.ts
71014
- function sendConnectionStatus(connection, status, reason) {
71014
+ function sendConnectionStatus(connection, status, reason, details) {
71015
71015
  connection.send(JSON.stringify({
71016
71016
  type: "connection_status",
71017
71017
  status,
71018
- reason
71018
+ reason,
71019
+ ...details
71019
71020
  }));
71020
71021
  }
71021
71022
  function sendAgentType(connection, agentType) {
@@ -71204,6 +71205,7 @@ const THINK_SKILLS_CONTEXT_LABEL = "think_skills";
71204
71205
  const SESSION_CONTEXT_SEPARATOR = "═".repeat(46);
71205
71206
  const TOOL_CONTINUATION_PREFIX = "economic:tool-continuation:";
71206
71207
  const TOOL_CONTINUATION_REQUEST_PREFIX = "economic:tool-continuation-request:";
71208
+ const CHAT_REFUSED = "You can read this chat, but you can't chat in it.";
71207
71209
  /** Whether a tool call's output is the paused or running output of an execution. */
71208
71210
  function showsExecution(part, executionId) {
71209
71211
  return "output" in part && part.output?.executionId === executionId;
@@ -71277,6 +71279,7 @@ var Agent = class Agent extends Think {
71277
71279
  this._tracingHandlers = this.getTracingHandlers();
71278
71280
  if (this.context.removeBlock(THINK_SKILLS_CONTEXT_LABEL)) await this.context.refreshSystemPrompt();
71279
71281
  this._unfinishedToolCalls = await this._toolTaskSettlements.unfinishedToolCalls();
71282
+ this._refuseChatMessages();
71280
71283
  this.registerInstance();
71281
71284
  }
71282
71285
  /**
@@ -71336,7 +71339,57 @@ var Agent = class Agent extends Think {
71336
71339
  connection.close(closeCode, message);
71337
71340
  return;
71338
71341
  }
71339
- sendConnectionStatus(connection, "connected");
71342
+ sendConnectionStatus(connection, "connected", void 0, { canChat: await this._askCanChat(connection) });
71343
+ }
71344
+ /**
71345
+ * Whether the user can send messages and answer approvals. When it's
71346
+ * `false` they can still read the chat. Asked for each connection in
71347
+ * `onConnect`, so an override of `onConnect` must call `super`. An error
71348
+ * counts as `false`. `true` by default.
71349
+ */
71350
+ canChat(_userContext) {
71351
+ return true;
71352
+ }
71353
+ /** Answers still being asked, by connection, so messages sent meanwhile wait for them. */
71354
+ _canChatAnswers = /* @__PURE__ */ new Map();
71355
+ /**
71356
+ * Asks `canChat` for a connection. A no marks it readonly, which outlives
71357
+ * hibernation and the user's token.
71358
+ */
71359
+ async _askCanChat(connection) {
71360
+ const answer = Promise.resolve().then(() => this.canChat(this.identity.userContext())).catch((error) => {
71361
+ logOperationalError("agent.can-chat.failed", error, this._logContext());
71362
+ return false;
71363
+ });
71364
+ this._canChatAnswers.set(connection.id, answer);
71365
+ const canChat = await answer;
71366
+ if (!canChat) this.setConnectionReadonly(connection, true);
71367
+ this._canChatAnswers.delete(connection.id);
71368
+ return canChat;
71369
+ }
71370
+ /** Whether the connection can chat. Calls from the server, without one, can. */
71371
+ async _canChat(connection = getCurrentAgent$1().connection) {
71372
+ if (!connection) return true;
71373
+ return await this._canChatAnswers.get(connection.id) ?? !this.isConnectionReadonly(connection);
71374
+ }
71375
+ /** Think stores a message before any hook runs, so it's refused before Think sees it. */
71376
+ _refuseChatMessages() {
71377
+ const onMessage = this.onMessage.bind(this);
71378
+ this.onMessage = async (connection, message) => {
71379
+ const event = typeof message === "string" ? parseProtocolMessage(message) : null;
71380
+ if (!((event?.type === "chat-request" || event?.type === "tool-approval" || event?.type === "tool-result" && event.autoContinue === true) && !this._cf_connectionTargetsSubAgent(connection) && !await this._canChat(connection))) return onMessage(connection, message);
71381
+ connection.send(JSON.stringify({
71382
+ type: CHAT_MESSAGE_TYPES.CHAT_MESSAGES,
71383
+ messages: this.messages
71384
+ }));
71385
+ if (event.type === "chat-request") connection.send(JSON.stringify({
71386
+ type: CHAT_MESSAGE_TYPES.USE_CHAT_RESPONSE,
71387
+ id: event.id,
71388
+ body: CHAT_REFUSED,
71389
+ done: true,
71390
+ error: true
71391
+ }));
71392
+ };
71340
71393
  }
71341
71394
  _turn = {
71342
71395
  inputTools: {},
@@ -71518,11 +71571,13 @@ var Agent = class Agent extends Think {
71518
71571
  }
71519
71572
  /** `all` also approves the tool call's later approvals, so it doesn't stop again. */
71520
71573
  async approveExecution(executionId, options) {
71574
+ if (!await this._canChat()) throw new Error(CHAT_REFUSED);
71521
71575
  const answer = await this._toolApprovals.answer(executionId, true, { all: options?.all });
71522
71576
  if (answer) return answer;
71523
71577
  return super.approveExecution(executionId);
71524
71578
  }
71525
71579
  async rejectExecution(executionId, reason, options) {
71580
+ if (!await this._canChat()) throw new Error(CHAT_REFUSED);
71526
71581
  const answer = await this._toolApprovals.answer(executionId, false, { autoContinue: options?.autoContinue });
71527
71582
  if (answer) return answer;
71528
71583
  return super.rejectExecution(executionId, reason, options);
@@ -73499,6 +73554,10 @@ var ChatAgent = class extends Agent {
73499
73554
  * The answer is the reply's text, or its JSON with `outputSchema`.
73500
73555
  */
73501
73556
  async run(input, body) {
73557
+ if (!await this._canChat()) return {
73558
+ status: "error",
73559
+ error: CHAT_REFUSED
73560
+ };
73502
73561
  const { inputSchema = z.string() } = await this.getAgentConfig(this.createToolContextForCurrentTurn(body));
73503
73562
  const check = checkInputValue(inputSchema, input);
73504
73563
  if (!check.success) return {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@economic/agents",
3
- "version": "3.1.0",
3
+ "version": "3.2.0",
4
4
  "description": "A starter for creating a TypeScript package.",
5
5
  "license": "MIT",
6
6
  "files": [