@standardagents/spec 0.40.13 → 0.41.1

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/dist/index.js CHANGED
@@ -283,104 +283,404 @@ function defineEffect(description, argsOrHandler, maybeHandler) {
283
283
  ];
284
284
  }
285
285
 
286
- // src/agents.ts
287
- function defineAgent(options) {
288
- const validateSessionBinding = (sideLabel, sessionKey) => {
289
- const side = options[sideLabel];
290
- if (!side) return;
291
- const sessionBinding = side[sessionKey];
292
- if (typeof sessionBinding === "object" && sessionBinding !== null && !Array.isArray(sessionBinding)) {
293
- if (!("name" in sessionBinding) || typeof sessionBinding.name !== "string" || !sessionBinding.name) {
294
- throw new Error(`${sideLabel}.${sessionKey}.name is required when ${sideLabel}.${sessionKey} uses object form`);
295
- }
296
- }
297
- };
298
- if (!options.name) {
299
- throw new Error("Agent name is required");
286
+ // src/handlers.ts
287
+ var HANDLER_SYMBOL = Symbol.for("standardagents.handler");
288
+ function defineHandler(options) {
289
+ if (!options || typeof options !== "object") {
290
+ throw new Error("defineHandler requires an options object");
300
291
  }
301
- if (options.name.includes("/")) {
302
- throw new Error(`Agent name cannot contain '/'. Reserved for namespace qualification.`);
292
+ if (typeof options.execute !== "function") {
293
+ throw new Error("Handler execute function is required");
303
294
  }
304
- if (!options.sideA) {
305
- throw new Error("Agent sideA configuration is required");
295
+ if (options.description !== void 0 && typeof options.description !== "string") {
296
+ throw new Error("Handler description must be a string when provided");
306
297
  }
307
- if (!options.sideA.prompt) {
308
- throw new Error("Agent sideA.prompt is required");
298
+ return {
299
+ ...options,
300
+ [HANDLER_SYMBOL]: true
301
+ };
302
+ }
303
+ function isHandler(value) {
304
+ return typeof value === "object" && value !== null && HANDLER_SYMBOL in value && value[HANDLER_SYMBOL] === true;
305
+ }
306
+
307
+ // src/validation.ts
308
+ var DEFINITION_ISSUE_CODES = [
309
+ "agent_name_missing",
310
+ "agent_name_invalid",
311
+ "agent_side_missing",
312
+ "agent_side_invalid",
313
+ "agent_side_backing_missing",
314
+ "agent_side_backing_conflict",
315
+ "agent_side_option_invalid_for_backing",
316
+ "agent_subagents_on_human_side",
317
+ "agent_both_sides_human",
318
+ "agent_legacy_type",
319
+ "agent_session_binding_legacy_string",
320
+ "agent_session_binding_invalid",
321
+ "agent_stop_tool_response_property_missing",
322
+ "agent_max_steps_invalid",
323
+ "agent_max_session_turns_invalid",
324
+ "agent_tool_description_missing",
325
+ "agent_parent_hooks_invalid",
326
+ "agent_tenvs_deprecated",
327
+ "subagent_agent_missing",
328
+ "subagent_relationship_invalid",
329
+ "subagent_relationship_duplicate",
330
+ "subagent_relationship_reserved_name",
331
+ "subagent_max_instances_invalid",
332
+ "subagent_legacy_name",
333
+ "subagent_legacy_resumable",
334
+ "subagent_legacy_receives_messages"
335
+ ];
336
+ var RESERVED_RELATIONSHIP_NAMES = [
337
+ "subagents",
338
+ "subagent_create",
339
+ "subagent_message"
340
+ ];
341
+ var MIGRATION_URL = "https://standardagentspec.com/0.2.0/getting-started/changelog";
342
+ var RESERVED_NAMES = new Set(
343
+ RESERVED_RELATIONSHIP_NAMES
344
+ );
345
+ function isRecord(value) {
346
+ return typeof value === "object" && value !== null && !Array.isArray(value);
347
+ }
348
+ var BACKING_KEYS = ["human", "prompt", "handler"];
349
+ var PROMPT_ONLY_SIDE_KEYS = [
350
+ "stopOnResponse",
351
+ "stopTool",
352
+ "stopToolResponseProperty",
353
+ "maxSteps",
354
+ "sessionStop",
355
+ "sessionFail",
356
+ "sessionStatus"
357
+ ];
358
+ function backingOf(side) {
359
+ const present = BACKING_KEYS.filter((key) => side[key] !== void 0);
360
+ return present.length === 1 ? present[0] : null;
361
+ }
362
+ function validateAgentDefinition(def) {
363
+ const issues = [];
364
+ const error = (code, path, message) => {
365
+ issues.push({ code, severity: "error", message, path });
366
+ };
367
+ const warning = (code, path, message) => {
368
+ issues.push({ code, severity: "warning", message, path });
369
+ };
370
+ if (!isRecord(def)) {
371
+ error("agent_side_invalid", "", "Agent definition must be an object");
372
+ return issues;
373
+ }
374
+ if (typeof def.name !== "string" || !def.name) {
375
+ error("agent_name_missing", "name", "Agent name is required");
376
+ } else if (def.name.includes("/")) {
377
+ error(
378
+ "agent_name_invalid",
379
+ "name",
380
+ `Agent name cannot contain '/'. Reserved for namespace qualification.`
381
+ );
382
+ }
383
+ if (def.type !== void 0) {
384
+ error(
385
+ "agent_legacy_type",
386
+ "type",
387
+ `'type' was removed in 0.2.0 \u2014 there is no agent type. Delete it and declare both sides explicitly: 'ai_human' becomes sideB: { human: true }; 'dual_ai' is two prompt-backed sides. ${MIGRATION_URL}`
388
+ );
309
389
  }
310
- const type = options.type ?? "ai_human";
311
- if (type === "dual_ai") {
312
- if (!options.sideB) {
313
- throw new Error("Agent sideB configuration is required for dual_ai type");
390
+ const relationshipOwners = /* @__PURE__ */ new Map();
391
+ for (const sideKey of ["sideA", "sideB"]) {
392
+ const side = def[sideKey];
393
+ if (side === void 0) {
394
+ error(
395
+ "agent_side_missing",
396
+ sideKey,
397
+ `${sideKey} is required \u2014 both sides are always explicit. A chat agent's human side is ${sideKey}: { human: true }.`
398
+ );
399
+ continue;
314
400
  }
315
- if (!options.sideB.prompt) {
316
- throw new Error("Agent sideB.prompt is required for dual_ai type");
401
+ if (!isRecord(side)) {
402
+ error("agent_side_invalid", sideKey, `${sideKey} must be an object`);
403
+ continue;
317
404
  }
318
- }
319
- if (options.exposeAsTool && !options.toolDescription) {
320
- throw new Error("toolDescription is required when exposeAsTool is true");
321
- }
322
- if (options.sideA.stopTool && !options.sideA.stopToolResponseProperty) {
323
- throw new Error("sideA.stopToolResponseProperty is required when sideA.stopTool is set");
324
- }
325
- if (options.sideB?.stopTool && !options.sideB.stopToolResponseProperty) {
326
- throw new Error("sideB.stopToolResponseProperty is required when sideB.stopTool is set");
327
- }
328
- if (options.sideA.maxSteps !== void 0 && options.sideA.maxSteps <= 0) {
329
- throw new Error("sideA.maxSteps must be a positive number");
330
- }
331
- if (options.sideB?.maxSteps !== void 0 && options.sideB.maxSteps <= 0) {
332
- throw new Error("sideB.maxSteps must be a positive number");
333
- }
334
- if (options.maxSessionTurns !== void 0 && options.maxSessionTurns !== null && options.maxSessionTurns <= 0) {
335
- throw new Error("maxSessionTurns must be a positive number");
336
- }
337
- if (!["ai_human", "dual_ai"].includes(type)) {
338
- throw new Error(`Invalid type '${type}'. Must be one of: ai_human, dual_ai`);
339
- }
340
- if (options.parentHooks !== void 0) {
341
- if (!Array.isArray(options.parentHooks)) {
342
- throw new Error("parentHooks must be an array");
405
+ const backing = backingOf(side);
406
+ if (backing === null) {
407
+ const present = BACKING_KEYS.filter((key) => side[key] !== void 0);
408
+ if (present.length === 0) {
409
+ error(
410
+ "agent_side_backing_missing",
411
+ sideKey,
412
+ `${sideKey} must be backed by exactly one of 'human', 'prompt', or 'handler'`
413
+ );
414
+ } else {
415
+ error(
416
+ "agent_side_backing_conflict",
417
+ sideKey,
418
+ `${sideKey} declares more than one backing (${present.join(", ")}); a side is backed by exactly one of 'human', 'prompt', or 'handler'`
419
+ );
420
+ }
421
+ continue;
422
+ }
423
+ if (backing === "human" && side.human !== true) {
424
+ error(
425
+ "agent_side_invalid",
426
+ `${sideKey}.human`,
427
+ `${sideKey}.human must be exactly true`
428
+ );
429
+ }
430
+ if (backing === "prompt" && (typeof side.prompt !== "string" || !side.prompt)) {
431
+ error(
432
+ "agent_side_invalid",
433
+ `${sideKey}.prompt`,
434
+ `${sideKey}.prompt must be a non-empty prompt name`
435
+ );
343
436
  }
344
- if (type !== "dual_ai") {
345
- throw new Error("parentHooks may only be declared by dual_ai agents");
437
+ if (backing === "handler" && (typeof side.handler !== "string" || !side.handler)) {
438
+ error(
439
+ "agent_side_invalid",
440
+ `${sideKey}.handler`,
441
+ `${sideKey}.handler must be a non-empty handler name`
442
+ );
346
443
  }
347
- if (!options.exposeAsTool) {
348
- throw new Error("parentHooks requires exposeAsTool: true");
444
+ if (backing !== "prompt") {
445
+ for (const key of PROMPT_ONLY_SIDE_KEYS) {
446
+ if (side[key] !== void 0) {
447
+ error(
448
+ "agent_side_option_invalid_for_backing",
449
+ `${sideKey}.${key}`,
450
+ backing === "human" ? `'${key}' is not legal on a human-backed side` : `'${key}' is not legal on a handler-backed side \u2014 a handler turn has no steps to cap and ends when execute returns; end sessions via state.stopSession()`
451
+ );
452
+ }
453
+ }
349
454
  }
350
- const seenParentHooks = /* @__PURE__ */ new Set();
351
- for (const requirement of options.parentHooks) {
352
- if (!requirement || typeof requirement !== "object") {
353
- throw new Error("parentHooks entries must be objects");
455
+ if (backing === "human" && side.subagents !== void 0) {
456
+ error(
457
+ "agent_subagents_on_human_side",
458
+ `${sideKey}.subagents`,
459
+ "A human-backed side cannot own subagent relationships \u2014 nothing there can invoke them. Declare them on the sibling side."
460
+ );
461
+ }
462
+ if (backing === "prompt") {
463
+ if (side.stopTool !== void 0 && side.stopToolResponseProperty === void 0) {
464
+ error(
465
+ "agent_stop_tool_response_property_missing",
466
+ `${sideKey}.stopToolResponseProperty`,
467
+ `${sideKey}.stopToolResponseProperty is required when ${sideKey}.stopTool is set`
468
+ );
354
469
  }
355
- if (!requirement.hook || typeof requirement.hook !== "string") {
356
- throw new Error("parentHooks[].hook is required");
470
+ if (side.maxSteps !== void 0 && (typeof side.maxSteps !== "number" || side.maxSteps <= 0)) {
471
+ error(
472
+ "agent_max_steps_invalid",
473
+ `${sideKey}.maxSteps`,
474
+ `${sideKey}.maxSteps must be a positive number`
475
+ );
357
476
  }
358
- if (typeof requirement.reason !== "string" || !requirement.reason.trim()) {
359
- throw new Error(`parentHooks reason is required for hook '${requirement.hook}'`);
477
+ for (const bindingKey of ["sessionStop", "sessionFail", "sessionStatus"]) {
478
+ const binding = side[bindingKey];
479
+ if (binding === void 0) continue;
480
+ if (typeof binding === "string") {
481
+ error(
482
+ "agent_session_binding_legacy_string",
483
+ `${sideKey}.${bindingKey}`,
484
+ `The bare-string session binding was removed in 0.2.0 \u2014 use the object form: ${bindingKey}: { name: '${binding}' }. ${MIGRATION_URL}`
485
+ );
486
+ continue;
487
+ }
488
+ if (!isRecord(binding) || typeof binding.name !== "string" || !binding.name) {
489
+ error(
490
+ "agent_session_binding_invalid",
491
+ `${sideKey}.${bindingKey}.name`,
492
+ `${sideKey}.${bindingKey}.name is required and must be a tool name`
493
+ );
494
+ }
360
495
  }
361
- if (requirement.stage !== void 0 && requirement.stage !== "before_parent" && requirement.stage !== "after_parent") {
362
- throw new Error(
363
- `Invalid parentHooks stage '${requirement.stage}' for hook '${requirement.hook}'`
496
+ }
497
+ if (backing !== "human" && side.subagents !== void 0) {
498
+ if (!Array.isArray(side.subagents)) {
499
+ error(
500
+ "subagent_relationship_invalid",
501
+ `${sideKey}.subagents`,
502
+ `${sideKey}.subagents must be an array of relationships`
364
503
  );
504
+ } else {
505
+ side.subagents.forEach((relationship, index) => {
506
+ const path = `${sideKey}.subagents[${index}]`;
507
+ if (!isRecord(relationship)) {
508
+ error(
509
+ "subagent_relationship_invalid",
510
+ path,
511
+ "Each subagent relationship must be an object"
512
+ );
513
+ return;
514
+ }
515
+ if (relationship.name !== void 0) {
516
+ error(
517
+ "subagent_legacy_name",
518
+ `${path}.name`,
519
+ `'name' was removed in 0.2.0 \u2014 the relationship name is 'as' (defaulting to the agent name) and the target agent is 'agent'. ${MIGRATION_URL}`
520
+ );
521
+ }
522
+ if (relationship.resumable !== void 0) {
523
+ error(
524
+ "subagent_legacy_resumable",
525
+ `${path}.resumable`,
526
+ `'resumable' was removed in 0.2.0 \u2014 every child instance is a persistent thread. Move maxInstances and parentCommunication to the relationship top level and delete receives_messages. ${MIGRATION_URL}`
527
+ );
528
+ }
529
+ if (relationship.receives_messages !== void 0) {
530
+ error(
531
+ "subagent_legacy_receives_messages",
532
+ `${path}.receives_messages`,
533
+ `'receives_messages' was removed in 0.2.0 \u2014 child traffic lands on the side that owns the relationship. Declare the relationship on the side that should receive the messages. ${MIGRATION_URL}`
534
+ );
535
+ }
536
+ if (typeof relationship.agent !== "string" || !relationship.agent) {
537
+ error(
538
+ "subagent_agent_missing",
539
+ `${path}.agent`,
540
+ `'agent' is required and must name the agent definition to instantiate`
541
+ );
542
+ return;
543
+ }
544
+ if (relationship.as !== void 0 && (typeof relationship.as !== "string" || !relationship.as)) {
545
+ error(
546
+ "subagent_relationship_invalid",
547
+ `${path}.as`,
548
+ `'as' must be a non-empty relationship name when provided`
549
+ );
550
+ return;
551
+ }
552
+ if (relationship.maxInstances !== void 0 && (typeof relationship.maxInstances !== "number" || relationship.maxInstances <= 0)) {
553
+ error(
554
+ "subagent_max_instances_invalid",
555
+ `${path}.maxInstances`,
556
+ `'maxInstances' must be a positive number when provided`
557
+ );
558
+ }
559
+ const relationshipName = typeof relationship.as === "string" && relationship.as ? relationship.as : relationship.agent;
560
+ if (RESERVED_NAMES.has(relationshipName)) {
561
+ error(
562
+ "subagent_relationship_reserved_name",
563
+ `${path}.as`,
564
+ `Relationship name '${relationshipName}' is reserved for the injected lifecycle tools (${RESERVED_RELATIONSHIP_NAMES.join(", ")}). Choose another 'as' name.`
565
+ );
566
+ }
567
+ const existingOwner = relationshipOwners.get(relationshipName);
568
+ if (existingOwner !== void 0) {
569
+ error(
570
+ "subagent_relationship_duplicate",
571
+ `${path}.as`,
572
+ existingOwner === sideKey ? `Relationship name '${relationshipName}' is declared twice on ${sideKey}. Give the second declaration a distinct 'as' alias.` : `Relationship name '${relationshipName}' is already owned by ${existingOwner} \u2014 a relationship name may appear on at most one side of an agent.`
573
+ );
574
+ } else {
575
+ relationshipOwners.set(relationshipName, sideKey);
576
+ }
577
+ });
365
578
  }
366
- if (seenParentHooks.has(requirement.hook)) {
367
- throw new Error(`Duplicate parentHooks hook '${requirement.hook}'`);
579
+ }
580
+ }
581
+ const sideA = isRecord(def.sideA) ? def.sideA : void 0;
582
+ const sideB = isRecord(def.sideB) ? def.sideB : void 0;
583
+ if (sideA !== void 0 && sideB !== void 0 && backingOf(sideA) === "human" && backingOf(sideB) === "human") {
584
+ error(
585
+ "agent_both_sides_human",
586
+ "sideB",
587
+ "Both sides human is reserved in this version \u2014 runtimes reject it. Back at least one side with a prompt or handler."
588
+ );
589
+ }
590
+ if (def.maxSessionTurns !== void 0 && def.maxSessionTurns !== null && (typeof def.maxSessionTurns !== "number" || def.maxSessionTurns <= 0)) {
591
+ error(
592
+ "agent_max_session_turns_invalid",
593
+ "maxSessionTurns",
594
+ "maxSessionTurns must be a positive number"
595
+ );
596
+ }
597
+ if (def.exposeAsTool === true && !def.toolDescription) {
598
+ error(
599
+ "agent_tool_description_missing",
600
+ "toolDescription",
601
+ "toolDescription is required when exposeAsTool is true"
602
+ );
603
+ }
604
+ if (def.tenvs !== void 0) {
605
+ warning(
606
+ "agent_tenvs_deprecated",
607
+ "tenvs",
608
+ `'tenvs' is deprecated \u2014 use 'env' instead`
609
+ );
610
+ }
611
+ if (def.parentHooks !== void 0) {
612
+ if (!Array.isArray(def.parentHooks)) {
613
+ error(
614
+ "agent_parent_hooks_invalid",
615
+ "parentHooks",
616
+ "parentHooks must be an array"
617
+ );
618
+ } else {
619
+ if (!def.exposeAsTool) {
620
+ error(
621
+ "agent_parent_hooks_invalid",
622
+ "parentHooks",
623
+ "parentHooks requires exposeAsTool: true \u2014 only a composable agent can require hooks on a parent"
624
+ );
368
625
  }
369
- seenParentHooks.add(requirement.hook);
626
+ const seen = /* @__PURE__ */ new Set();
627
+ def.parentHooks.forEach((requirement, index) => {
628
+ const path = `parentHooks[${index}]`;
629
+ if (!isRecord(requirement)) {
630
+ error(
631
+ "agent_parent_hooks_invalid",
632
+ path,
633
+ "parentHooks entries must be objects"
634
+ );
635
+ return;
636
+ }
637
+ if (typeof requirement.hook !== "string" || !requirement.hook) {
638
+ error(
639
+ "agent_parent_hooks_invalid",
640
+ `${path}.hook`,
641
+ "parentHooks[].hook is required"
642
+ );
643
+ return;
644
+ }
645
+ if (typeof requirement.reason !== "string" || !requirement.reason.trim()) {
646
+ error(
647
+ "agent_parent_hooks_invalid",
648
+ `${path}.reason`,
649
+ `parentHooks reason is required for hook '${requirement.hook}'`
650
+ );
651
+ }
652
+ if (requirement.stage !== void 0 && requirement.stage !== "before_parent" && requirement.stage !== "after_parent") {
653
+ error(
654
+ "agent_parent_hooks_invalid",
655
+ `${path}.stage`,
656
+ `Invalid parentHooks stage '${String(requirement.stage)}' for hook '${requirement.hook}'`
657
+ );
658
+ }
659
+ if (seen.has(requirement.hook)) {
660
+ error(
661
+ "agent_parent_hooks_invalid",
662
+ `${path}.hook`,
663
+ `Duplicate parentHooks hook '${requirement.hook}'`
664
+ );
665
+ }
666
+ seen.add(requirement.hook);
667
+ });
370
668
  }
371
669
  }
372
- validateSessionBinding("sideA", "sessionStop");
373
- validateSessionBinding("sideA", "sessionFail");
374
- validateSessionBinding("sideA", "sessionStatus");
375
- if (options.sideB) {
376
- validateSessionBinding("sideB", "sessionStop");
377
- validateSessionBinding("sideB", "sessionFail");
378
- validateSessionBinding("sideB", "sessionStatus");
670
+ return issues;
671
+ }
672
+
673
+ // src/agents.ts
674
+ function defineAgent(options) {
675
+ const errors = validateAgentDefinition(options).filter(
676
+ (issue) => issue.severity === "error"
677
+ );
678
+ if (errors.length > 0) {
679
+ throw new Error(
680
+ errors.map((issue) => `${issue.path}: ${issue.message}`).join("\n")
681
+ );
379
682
  }
380
- return {
381
- ...options,
382
- type
383
- };
683
+ return options;
384
684
  }
385
685
 
386
686
  // src/threads.ts
@@ -482,9 +782,16 @@ function isVisibleInNamespace(metadata, namespace, isEntryPoint = false) {
482
782
  const isOwnPackage = hasPkg && metadata.__package.packageId === namespace.packageId;
483
783
  return isOwnPackage || hasPkg && isEntryPoint;
484
784
  }
785
+
786
+ // src/index.ts
787
+ var SPEC_VERSION = "0.2.0";
485
788
  export {
789
+ DEFINITION_ISSUE_CODES,
486
790
  ENDPOINT_ACCESS_SYMBOL,
791
+ HANDLER_SYMBOL,
487
792
  ProviderError,
793
+ RESERVED_RELATIONSHIP_NAMES,
794
+ SPEC_VERSION,
488
795
  THREAD_ENDPOINT_SYMBOL,
489
796
  THREAD_STREAM_REASONING_PARAM,
490
797
  USER_ENDPOINT_SYMBOL,
@@ -492,6 +799,7 @@ export {
492
799
  defineAgent,
493
800
  defineController,
494
801
  defineEffect,
802
+ defineHandler,
495
803
  defineHook,
496
804
  defineModel,
497
805
  definePrompt,
@@ -500,6 +808,7 @@ export {
500
808
  defineTool,
501
809
  defineUserEndpoint,
502
810
  getEndpointAccess,
811
+ isHandler,
503
812
  isPacked,
504
813
  isThreadEndpoint,
505
814
  isUserEndpoint,
@@ -508,6 +817,7 @@ export {
508
817
  providerConst,
509
818
  providerEnv,
510
819
  providerValue,
820
+ validateAgentDefinition,
511
821
  validateToolName
512
822
  };
513
823
  //# sourceMappingURL=index.js.map