@economic/agents 2.13.0 → 2.14.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
@@ -421,6 +421,7 @@ import { z } from "zod";
421
421
 
422
422
  export const weatherSkill = skill({
423
423
  name: "weather",
424
+ command: "weather",
424
425
  description: "Look up current weather and forecasts. Use for any weather question.",
425
426
  instructions: `
426
427
  # Weather
@@ -445,6 +446,16 @@ Use this skill whenever the user asks about current conditions or a forecast.
445
446
 
446
447
  Return skills from `getSkills()`. Add `authorize(ctx)` to gate a skill (and its tools). Denied skills are hidden from the catalog and `activate_skill` by default. Set `onUnauthorized` to a reason string, or a function that returns one, to expose a denied skill with user-facing guidance. A function can return `undefined` to keep it hidden. `skill()` throws if `description` or `instructions` is missing.
447
448
 
449
+ Set `command` to make a code-defined skill directly invocable from chat. For example,
450
+ `/weather in Copenhagen` loads the weather instructions and enables its tools before the
451
+ first model step; the model still interprets `in Copenhagen` and chooses the appropriate
452
+ tool. Commands use lower-case letters, numbers, and hyphens, without the leading `/`.
453
+
454
+ `ChatAgent` also exposes an authenticated `GET getInvocableSkills` action for command
455
+ pickers. It returns the commands available to the current user as
456
+ `{ command, name, description }[]`. The existing skill `description` is used for both the
457
+ model catalog and the human-facing picker.
458
+
448
459
  ### Native Agent Skills
449
460
 
450
461
  Native Agent Skills are folders containing a `SKILL.md` file, with optional references and scripts:
package/dist/index.d.mts CHANGED
@@ -153,6 +153,11 @@ type AnalyticsEventPayload = {
153
153
  durationMs: number;
154
154
  inputSize: number;
155
155
  outputSize: number;
156
+ } | {
157
+ type: "skill.invoked";
158
+ skillName: string;
159
+ command: string;
160
+ source: "slash-command";
156
161
  } | {
157
162
  type: "feedback.submitted";
158
163
  messageId: string;
@@ -207,6 +212,7 @@ declare class Analytics {
207
212
  promptReceived(messages: readonly ModelMessage[], continuation: boolean): void;
208
213
  turnCompleted(requestId: string, status: string, continuation: boolean, response: unknown): void;
209
214
  feedbackSubmitted(messageId: string, rating: 1 | -1 | 0): void;
215
+ skillInvoked(skillName: string, command: string): void;
210
216
  event(payload: AnalyticsEventPayload | (() => AnalyticsEventPayload)): void;
211
217
  }
212
218
  //#endregion
@@ -432,12 +438,69 @@ type MessageFeedback = {
432
438
  updated_at: number;
433
439
  };
434
440
  //#endregion
441
+ //#region src/server/util/actions.d.ts
442
+ /** Tool execution options plus the additional context supplied by native Think actions. */
443
+ type ActionContext<Context extends Record<string, unknown> = Record<string, unknown>> = Omit<ToolExecutionOptions, "experimental_context"> & ActionContext$1 & {
444
+ experimental_context: Context; /** The action's resolved domain key, or its tool-call fallback when none was declared. */
445
+ idempotencyKey: string;
446
+ };
447
+ type Action<Context extends Record<string, unknown> = Record<string, unknown>, Input = any, Output = any> = Omit<ActionConfig<FlexibleSchema<Input>, Output>, "approval" | "execute" | "idempotencyKey" | "inputSchema" | "permissions"> & {
448
+ description: string;
449
+ inputSchema: FlexibleSchema<Input>;
450
+ approval?: ActionApprovalPolicy<Input>;
451
+ idempotencyKey?: ActionIdempotencyKey<Input>;
452
+ permissions?: ActionPermissionSpec<Input>;
453
+ authorize?: (ctx?: Context) => boolean;
454
+ onUnauthorized?: UnauthorizedPolicy<Context>;
455
+ execute(input: Input, ctx: ActionContext<Context>): Promise<Output> | Output;
456
+ };
457
+ type RegisteredAction<Context extends Record<string, unknown> = Record<string, unknown>, InputSchema extends FlexibleSchema = FlexibleSchema, Output = unknown> = Action$1<InputSchema, Output> & {
458
+ authorize?: (ctx?: Context) => boolean;
459
+ onUnauthorized?: UnauthorizedPolicy<Context>;
460
+ };
461
+ /** A native Think action with the SDK's standard authorization policy. */
462
+ declare function action<Context extends Record<string, unknown> = Record<string, unknown>, Input = any, Output = any>(definition: Action<Context, Input, Output>): RegisteredAction<Context, FlexibleSchema<Input>, Output>;
463
+ //#endregion
464
+ //#region src/server/util/skills.d.ts
465
+ interface Skill<Context extends Record<string, unknown> = Record<string, unknown>> {
466
+ name: string;
467
+ description: string;
468
+ instructions: string;
469
+ /** Enables direct invocation from chat with `/command`. */
470
+ command?: string;
471
+ tools?: Record<string, Tool<Context>>;
472
+ actions?: Record<string, RegisteredAction<Context, any, any>>;
473
+ authorize?: (ctx?: Context) => boolean;
474
+ onUnauthorized?: UnauthorizedPolicy<Context>;
475
+ }
476
+ interface SkillSource<Context extends Record<string, unknown> = Record<string, unknown>> extends SkillSource$1 {
477
+ command?: string;
478
+ tools?: Record<string, Tool<Context>>;
479
+ actions?: Record<string, RegisteredAction<Context, any, any>>;
480
+ authorize?: (ctx?: Context | undefined) => boolean;
481
+ onUnauthorized?: UnauthorizedPolicy<Context>;
482
+ }
483
+ declare function skill<Context extends Record<string, unknown> = Record<string, unknown>>(definition: Skill<Context>): SkillSource<Context>;
484
+ //#endregion
485
+ //#region src/server/util/skillCommands.d.ts
486
+ interface InvocableSkill {
487
+ /** Slash command without the leading `/`. */
488
+ command: string;
489
+ /** Name of the skill the command activates. */
490
+ name: string;
491
+ /** Human- and model-readable summary from the skill definition. */
492
+ description: string;
493
+ }
494
+ //#endregion
435
495
  //#region src/server/agents/ChatAgent.d.ts
436
496
  declare abstract class ChatAgent<RequestContext extends Record<string, unknown> = Record<string, unknown>, UserContext extends Record<string, unknown> = Record<string, unknown>, State = unknown> extends Agent<RequestContext, UserContext, State> {
437
497
  protected agentType: AgentConnectionType;
438
498
  onStart(): Promise<void>;
439
499
  configureSession(session: Session): Session;
440
500
  beforeTurn(ctx: TurnContext): Promise<void | TurnConfig>;
501
+ private getSlashCommandSkill;
502
+ private invokeSkill;
503
+ protected getInvocableSkills(userContext?: UserContext | undefined, requestContext?: RequestContext): Promise<InvocableSkill[]>;
441
504
  onChatResponse(result: ChatResponseResult): Promise<void>;
442
505
  onRequest(request: Request): Promise<Response>;
443
506
  /**
@@ -512,51 +575,10 @@ declare abstract class Assistant<State = unknown> extends Agent$1<Cloudflare.Env
512
575
  observability?: Observability;
513
576
  }
514
577
  //#endregion
515
- //#region src/server/util/actions.d.ts
516
- /** Tool execution options plus the additional context supplied by native Think actions. */
517
- type ActionContext<Context extends Record<string, unknown> = Record<string, unknown>> = Omit<ToolExecutionOptions, "experimental_context"> & ActionContext$1 & {
518
- experimental_context: Context; /** The action's resolved domain key, or its tool-call fallback when none was declared. */
519
- idempotencyKey: string;
520
- };
521
- type Action<Context extends Record<string, unknown> = Record<string, unknown>, Input = any, Output = any> = Omit<ActionConfig<FlexibleSchema<Input>, Output>, "approval" | "execute" | "idempotencyKey" | "inputSchema" | "permissions"> & {
522
- description: string;
523
- inputSchema: FlexibleSchema<Input>;
524
- approval?: ActionApprovalPolicy<Input>;
525
- idempotencyKey?: ActionIdempotencyKey<Input>;
526
- permissions?: ActionPermissionSpec<Input>;
527
- authorize?: (ctx?: Context) => boolean;
528
- onUnauthorized?: UnauthorizedPolicy<Context>;
529
- execute(input: Input, ctx: ActionContext<Context>): Promise<Output> | Output;
530
- };
531
- type RegisteredAction<Context extends Record<string, unknown> = Record<string, unknown>, InputSchema extends FlexibleSchema = FlexibleSchema, Output = unknown> = Action$1<InputSchema, Output> & {
532
- authorize?: (ctx?: Context) => boolean;
533
- onUnauthorized?: UnauthorizedPolicy<Context>;
534
- };
535
- /** A native Think action with the SDK's standard authorization policy. */
536
- declare function action<Context extends Record<string, unknown> = Record<string, unknown>, Input = any, Output = any>(definition: Action<Context, Input, Output>): RegisteredAction<Context, FlexibleSchema<Input>, Output>;
537
- //#endregion
538
- //#region src/server/util/skills.d.ts
539
- interface Skill<Context extends Record<string, unknown> = Record<string, unknown>> {
540
- name: string;
541
- description: string;
542
- instructions: string;
543
- tools?: Record<string, Tool<Context>>;
544
- actions?: Record<string, RegisteredAction<Context, any, any>>;
545
- authorize?: (ctx?: Context) => boolean;
546
- onUnauthorized?: UnauthorizedPolicy<Context>;
547
- }
548
- interface SkillSource<Context extends Record<string, unknown> = Record<string, unknown>> extends SkillSource$1 {
549
- tools?: Record<string, Tool<Context>>;
550
- actions?: Record<string, RegisteredAction<Context, any, any>>;
551
- authorize?: (ctx?: Context | undefined) => boolean;
552
- onUnauthorized?: UnauthorizedPolicy<Context>;
553
- }
554
- declare function skill<Context extends Record<string, unknown> = Record<string, unknown>>(definition: Skill<Context>): SkillSource<Context>;
555
- //#endregion
556
578
  //#region src/server/route-agent-request.d.ts
557
579
  declare function routeAgentRequest<Env>(request: Request, env: Env, options?: AgentOptions<Env>): Promise<Response | null>;
558
580
  //#endregion
559
581
  //#region src/server/index.d.ts
560
582
  declare function getCurrentToolContext(): any;
561
583
  //#endregion
562
- export { type Action, type ActionContext, Agent, type AgentConnectionStatus, type AgentConnectionType, type AgentEnv, type AnalyticsAttribute, type AnalyticsAttributes, type AnalyticsEvent, type AnalyticsEventPayload, type AnalyticsHandler, type AnalyticsUsage, Assistant, ChatAgent, type ExchangedToken, type JwtAuthConfig, type RegisteredAction, type Skill, type SkillSource, type TokenExchangeConfig, type TokenExchangeResult, type Tool, type ToolContext, type ToolSet, type TurnSnapshot, type UnauthorizedPolicy, type UnauthorizedToolPolicy, action, createAnalyticsEngineHandler, createAnalyticsEvent, createJwtAuthConfig, dispatchAnalyticsEvent, exchangeToken, getCurrentToolContext, routeAgentRequest, skill, tool };
584
+ export { type Action, type ActionContext, Agent, type AgentConnectionStatus, type AgentConnectionType, type AgentEnv, type AnalyticsAttribute, type AnalyticsAttributes, type AnalyticsEvent, type AnalyticsEventPayload, type AnalyticsHandler, type AnalyticsUsage, Assistant, ChatAgent, type ExchangedToken, type InvocableSkill, type JwtAuthConfig, type RegisteredAction, type Skill, type SkillSource, type TokenExchangeConfig, type TokenExchangeResult, type Tool, type ToolContext, type ToolSet, type TurnSnapshot, type UnauthorizedPolicy, type UnauthorizedToolPolicy, action, createAnalyticsEngineHandler, createAnalyticsEvent, createJwtAuthConfig, dispatchAnalyticsEvent, exchangeToken, getCurrentToolContext, routeAgentRequest, skill, tool };
package/dist/index.mjs CHANGED
@@ -905,6 +905,19 @@ function toAnalyticsEnginePoints(event) {
905
905
  0
906
906
  ]
907
907
  }));
908
+ case "skill.invoked": return [{
909
+ indexes,
910
+ blobs: [
911
+ "skill_invoked",
912
+ event.agentName,
913
+ event.durableObjectName,
914
+ event.skillName,
915
+ event.command,
916
+ event.source,
917
+ ...common
918
+ ],
919
+ doubles: [1]
920
+ }];
908
921
  case "feedback.submitted": return [{
909
922
  indexes,
910
923
  blobs: [
@@ -998,6 +1011,14 @@ var Analytics = class {
998
1011
  rating
999
1012
  });
1000
1013
  }
1014
+ skillInvoked(skillName, command) {
1015
+ this.emit({
1016
+ type: "skill.invoked",
1017
+ skillName,
1018
+ command,
1019
+ source: "slash-command"
1020
+ });
1021
+ }
1001
1022
  event(payload) {
1002
1023
  this.emit(payload);
1003
1024
  }
@@ -1161,7 +1182,6 @@ var Agent = class extends Think {
1161
1182
  const { tools, activeTools } = await this._prepareTools(ctx.tools, activeSkillName, true);
1162
1183
  const baseSystem = await this.session.refreshSystemPrompt();
1163
1184
  this._turn.baseSystem = [baseSystem, this._turn.skillsSystemBlock].filter(Boolean).join("\n\n");
1164
- console.log(this._turn.baseSystem, "\n\n// end system prompt\n\n");
1165
1185
  return {
1166
1186
  system: this._turn.baseSystem,
1167
1187
  model: this.getModel(this._turn.context),
@@ -2338,6 +2358,38 @@ var Assistant = class extends Agent$1 {
2338
2358
  agentClass: this.constructor.name
2339
2359
  }) });
2340
2360
  };
2361
+ //#endregion
2362
+ //#region src/server/util/skillCommands.ts
2363
+ async function listInvocableSkills(sources) {
2364
+ const commands = /* @__PURE__ */ new Set();
2365
+ const skills = [];
2366
+ for (const source of sources) {
2367
+ if (!source.command) continue;
2368
+ if (commands.has(source.command)) throw new Error(`Duplicate skill command "/${source.command}"`);
2369
+ const descriptor = (await source.list()).find((skill) => skill.name === source.id);
2370
+ if (!descriptor) continue;
2371
+ commands.add(source.command);
2372
+ skills.push({
2373
+ command: source.command,
2374
+ name: descriptor.name,
2375
+ description: descriptor.description
2376
+ });
2377
+ }
2378
+ return skills;
2379
+ }
2380
+ function parseSkillCommand(messages, skills) {
2381
+ const message = messages.findLast((candidate) => candidate.role === "user");
2382
+ if (!message) return;
2383
+ const text = getMessageText(message);
2384
+ if (!text?.startsWith("/")) return;
2385
+ const match = /^\/([^\s]+)/.exec(text);
2386
+ if (!match) return;
2387
+ return skills.find((skill) => skill.command === match[1]);
2388
+ }
2389
+ function getMessageText(message) {
2390
+ if (typeof message.content === "string") return message.content;
2391
+ return message.content.filter((part) => part.type === "text").map((part) => part.text).join("") || void 0;
2392
+ }
2341
2393
  const COMPACTION_MAX_OUTPUT_TOKENS = 2e3;
2342
2394
  const createCompactFn = (model) => createCompactFunction({
2343
2395
  protectHead: 2,
@@ -2675,7 +2727,22 @@ var ChatAgent = class extends Agent {
2675
2727
  async beforeTurn(ctx) {
2676
2728
  const parent = await this.getParentAgent();
2677
2729
  if (parent?.registerChat) await parent.registerChat(this.name);
2678
- const config = await super.beforeTurn(ctx) ?? {};
2730
+ const slashCommandSkill = await this.getSlashCommandSkill(ctx);
2731
+ let skillInvocation;
2732
+ if (slashCommandSkill) skillInvocation = await this.invokeSkill(ctx, slashCommandSkill);
2733
+ const turnContext = skillInvocation ? {
2734
+ ...ctx,
2735
+ messages: [...ctx.messages, ...skillInvocation.messages]
2736
+ } : ctx;
2737
+ let config = await super.beforeTurn(turnContext) ?? {};
2738
+ if (skillInvocation) {
2739
+ const transforms = config.experimental_transform ? Array.isArray(config.experimental_transform) ? config.experimental_transform : [config.experimental_transform] : [];
2740
+ config = {
2741
+ ...config,
2742
+ activeTools: config.activeTools?.filter((name) => name !== "activate_skill"),
2743
+ experimental_transform: [skillInvocation.transform, ...transforms]
2744
+ };
2745
+ }
2679
2746
  try {
2680
2747
  const prepared = await prepareModelHistory(this.messages, getModelCompaction(this.sql.bind(this)), createCompactFn(config.model ?? ctx.model), config.system ?? ctx.system);
2681
2748
  if (prepared.stateChanged) if (prepared.state) setModelCompaction(this.sql.bind(this), prepared.state);
@@ -2691,7 +2758,7 @@ var ChatAgent = class extends Agent {
2691
2758
  });
2692
2759
  return {
2693
2760
  ...config,
2694
- messages: this.pruneTurnMessages(modelMessages)
2761
+ messages: this.pruneTurnMessages([...modelMessages, ...skillInvocation?.messages ?? []])
2695
2762
  };
2696
2763
  }
2697
2764
  } catch (error) {
@@ -2699,6 +2766,67 @@ var ChatAgent = class extends Agent {
2699
2766
  }
2700
2767
  return config;
2701
2768
  }
2769
+ async getSlashCommandSkill(context) {
2770
+ if (context.continuation) return;
2771
+ const connectionState = getCurrentAgent()?.connection?.state;
2772
+ const invocableSkills = await this.getInvocableSkills(connectionState?.userContext ?? this.userContext, context.body);
2773
+ return parseSkillCommand(context.messages, invocableSkills);
2774
+ }
2775
+ async invokeSkill(context, skill) {
2776
+ const activateSkill = context.tools.activate_skill;
2777
+ if (!activateSkill?.execute) return;
2778
+ const input = { name: skill.name };
2779
+ const toolCallId = crypto.randomUUID();
2780
+ const output = await activateSkill.execute(input, {
2781
+ toolCallId,
2782
+ messages: context.messages
2783
+ });
2784
+ if (typeof output !== "string") throw new Error("activate_skill returned an invalid result");
2785
+ this.analytics.skillInvoked(skill.name, skill.command);
2786
+ const messages = await convertToModelMessages([{
2787
+ id: crypto.randomUUID(),
2788
+ role: "assistant",
2789
+ parts: [{
2790
+ type: "tool-activate_skill",
2791
+ toolCallId,
2792
+ state: "output-available",
2793
+ input,
2794
+ output
2795
+ }]
2796
+ }], { tools: context.tools });
2797
+ const transform = () => {
2798
+ let emitted = false;
2799
+ return new TransformStream({ transform(chunk, controller) {
2800
+ controller.enqueue(chunk);
2801
+ if (emitted || chunk.type !== "start-step") return;
2802
+ emitted = true;
2803
+ controller.enqueue({
2804
+ type: "tool-call",
2805
+ toolCallId,
2806
+ toolName: "activate_skill",
2807
+ input
2808
+ });
2809
+ controller.enqueue({
2810
+ type: "tool-result",
2811
+ toolCallId,
2812
+ toolName: "activate_skill",
2813
+ input,
2814
+ output
2815
+ });
2816
+ } });
2817
+ };
2818
+ return {
2819
+ messages,
2820
+ transform
2821
+ };
2822
+ }
2823
+ async getInvocableSkills(userContext = this.userContext, requestContext = {}) {
2824
+ const context = {
2825
+ ...requestContext,
2826
+ _userContext: userContext
2827
+ };
2828
+ return listInvocableSkills((await this.getSkills()).filter((source) => source.authorize?.(context) !== false));
2829
+ }
2702
2830
  async onChatResponse(result) {
2703
2831
  this.analytics.turnCompleted(result.requestId, result.status, result.continuation, result.message.parts);
2704
2832
  const parent = await this.getParentAgent();
@@ -2709,6 +2837,7 @@ var ChatAgent = class extends Agent {
2709
2837
  if (!auth.success) return Response.json({ error: auth.message }, { status: auth.status });
2710
2838
  const action = new URL(request.url).pathname.split("/").pop();
2711
2839
  if (action === "getMessages" && request.method === "GET") return Response.json(await this.getMessages());
2840
+ if (action === "getInvocableSkills" && request.method === "GET") return Response.json(await this.getInvocableSkills(auth.userContext));
2712
2841
  if (action === "sendMessage" && request.method === "POST") return this.streamMessage(request);
2713
2842
  if (action === "cancelChat" && request.method === "POST") {
2714
2843
  const { requestId } = await request.json();
@@ -2828,6 +2957,7 @@ function skill(definition) {
2828
2957
  if (!definition.name) throw new Error("Skill name is required");
2829
2958
  if (!definition.description) throw new Error("Skill description is required");
2830
2959
  if (!definition.instructions) throw new Error("Skill content is required");
2960
+ if (definition.command !== void 0 && !/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(definition.command)) throw new Error("Skill command must use lower-case letters, numbers, and hyphens");
2831
2961
  return {
2832
2962
  id: definition.name,
2833
2963
  fingerprint: definition.name,
@@ -2845,6 +2975,7 @@ function skill(definition) {
2845
2975
  body: definition.instructions
2846
2976
  };
2847
2977
  },
2978
+ command: definition.command,
2848
2979
  tools: definition.tools,
2849
2980
  actions: definition.actions,
2850
2981
  authorize: definition.authorize,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@economic/agents",
3
- "version": "2.13.0",
3
+ "version": "2.14.0",
4
4
  "description": "A starter for creating a TypeScript package.",
5
5
  "license": "MIT",
6
6
  "files": [
@@ -45,7 +45,7 @@
45
45
  },
46
46
  "peerDependencies": {
47
47
  "@cloudflare/think": "^0.17.0",
48
- "@cloudflare/vite-plugin": "^1.43.0 <1.43.2",
48
+ "@cloudflare/vite-plugin": "^1.45.1",
49
49
  "@cloudflare/worker-bundler": "^0.2.1",
50
50
  "agents": "^0.22.0",
51
51
  "vite": ">=8.0.0"