@agentfield/sdk 0.1.137-rc.4 → 0.1.137-rc.6

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
@@ -1,5 +1,9 @@
1
1
  # AgentField TypeScript SDK
2
2
 
3
+ ## Graceful shutdown
4
+
5
+ `serve()` installs SIGTERM and SIGINT handlers that notify the control plane and drain detached executions. Host processes that own signal handling can use `serve({ handleSignals: false })` and call the idempotent `shutdown()` method themselves. `AGENTFIELD_SHUTDOWN_TIMEOUT` accepts bare seconds (`30`) or durations (`30s`, `5m`) and defaults to 30 seconds. In Kubernetes, set `terminationGracePeriodSeconds` higher than this timeout.
6
+
3
7
  The TypeScript SDK provides an idiomatic Node.js interface for building and running AgentField agents. It mirrors the Python SDK APIs, including AI, memory, discovery, and MCP tooling.
4
8
 
5
9
  ## Installing
package/dist/index.d.ts CHANGED
@@ -792,6 +792,7 @@ declare class AgentFieldClient {
792
792
  constructor(config: AgentConfig);
793
793
  register(payload: any): Promise<any>;
794
794
  getNode(nodeId: string): Promise<any>;
795
+ shutdown(nodeId: string): Promise<any>;
795
796
  heartbeat(status?: 'starting' | 'ready' | 'degraded' | 'offline'): Promise<HealthStatus>;
796
797
  execute<T = any>(target: string, input: any, metadata?: {
797
798
  runId?: string;
@@ -1949,6 +1950,10 @@ declare class RealtimeSession {
1949
1950
  }
1950
1951
  declare function buildSessionDefinition(name: string, options: SessionOptions): SessionDefinition;
1951
1952
 
1953
+ interface ServeOptions {
1954
+ handleSignals?: boolean;
1955
+ }
1956
+
1952
1957
  declare class Agent {
1953
1958
  readonly config: AgentConfig;
1954
1959
  readonly app: express.Express;
@@ -1956,6 +1961,10 @@ declare class Agent {
1956
1961
  readonly skills: SkillRegistry;
1957
1962
  private server?;
1958
1963
  private heartbeatTimer?;
1964
+ private shutdownPromise?;
1965
+ private readonly inFlightExecutions;
1966
+ private shuttingDown;
1967
+ private signalHandlers?;
1959
1968
  private readonly aiClient;
1960
1969
  private readonly agentFieldClient;
1961
1970
  private readonly memoryClient;
@@ -2074,8 +2083,10 @@ declare class Agent {
2074
2083
  executionId?: string;
2075
2084
  }): Promise<ApprovalResult>;
2076
2085
  private buildExecutionLogContext;
2077
- serve(): Promise<void>;
2086
+ serve(options?: ServeOptions): Promise<void>;
2078
2087
  shutdown(): Promise<void>;
2088
+ private performShutdown;
2089
+ private installSignalHandlers;
2079
2090
  call(target: string, input: any): Promise<any>;
2080
2091
  /**
2081
2092
  * Remote call variant that submits the execution asynchronously and polls for
package/dist/index.js CHANGED
@@ -4161,6 +4161,14 @@ var AgentFieldClient = class {
4161
4161
  });
4162
4162
  return res.data;
4163
4163
  }
4164
+ async shutdown(nodeId) {
4165
+ const bodyStr = JSON.stringify({ reason: "shutdown", version: this.config.version ?? "" });
4166
+ const authHeaders = this.didAuthenticator.signRequest(Buffer.from(bodyStr));
4167
+ const res = await this.http.post(`/api/v1/nodes/${encodeURIComponent(nodeId)}/shutdown`, bodyStr, {
4168
+ headers: this.mergeHeaders({ "Content-Type": "application/json", ...authHeaders })
4169
+ });
4170
+ return res.data;
4171
+ }
4164
4172
  async heartbeat(status = "ready") {
4165
4173
  const nodeId = this.config.nodeId;
4166
4174
  const bodyStr = JSON.stringify({ status, version: this.config.version ?? "", timestamp: (/* @__PURE__ */ new Date()).toISOString() });
@@ -6041,10 +6049,26 @@ function buildSessionDefinition(name, options) {
6041
6049
  };
6042
6050
  }
6043
6051
 
6052
+ // src/agent/signals.ts
6053
+ var DEFAULT_SHUTDOWN_TIMEOUT_MS = 3e4;
6054
+ function parseShutdownTimeout(value, warn = console.warn) {
6055
+ if (!value?.trim()) return DEFAULT_SHUTDOWN_TIMEOUT_MS;
6056
+ const text2 = value.trim();
6057
+ if (/^\d+$/.test(text2)) return Number(text2) * 1e3;
6058
+ const match = text2.match(/^(\d+(?:\.\d+)?)(ms|s|m|h)$/);
6059
+ if (match) {
6060
+ const factors = { ms: 1, s: 1e3, m: 6e4, h: 36e5 };
6061
+ return Number(match[1]) * factors[match[2]];
6062
+ }
6063
+ warn(`invalid AGENTFIELD_SHUTDOWN_TIMEOUT ${JSON.stringify(value)}; using 30s`);
6064
+ return DEFAULT_SHUTDOWN_TIMEOUT_MS;
6065
+ }
6066
+
6044
6067
  // src/agent/Agent.ts
6045
6068
  var TargetNotFoundError = class extends Error {
6046
6069
  };
6047
6070
  var AGENTFIELD_TS_SDK_VERSION = "0.1.82";
6071
+ var POST_CANCEL_SETTLEMENT_MS = 5e3;
6048
6072
  var harnessRunners = /* @__PURE__ */ new WeakMap();
6049
6073
  function normalizeAcceptsWebhook(value) {
6050
6074
  if (value === true) return "true";
@@ -6074,6 +6098,10 @@ var Agent = class {
6074
6098
  skills = new SkillRegistry();
6075
6099
  server;
6076
6100
  heartbeatTimer;
6101
+ shutdownPromise;
6102
+ inFlightExecutions = /* @__PURE__ */ new Map();
6103
+ shuttingDown = false;
6104
+ signalHandlers;
6077
6105
  aiClient;
6078
6106
  agentFieldClient;
6079
6107
  memoryClient;
@@ -6473,7 +6501,7 @@ var Agent = class {
6473
6501
  agentNodeDid: current.agentNodeDid
6474
6502
  };
6475
6503
  }
6476
- async serve() {
6504
+ async serve(options = {}) {
6477
6505
  await this.registerWithControlPlane();
6478
6506
  if (this.localVerifier) {
6479
6507
  try {
@@ -6493,19 +6521,70 @@ var Agent = class {
6493
6521
  });
6494
6522
  this.memoryEventClient.start();
6495
6523
  this.startHeartbeat();
6496
- }
6497
- async shutdown() {
6498
- if (this.heartbeatTimer) {
6499
- clearInterval(this.heartbeatTimer);
6500
- }
6501
- this.pauseManager.cancelAll();
6502
- await new Promise((resolve3, reject) => {
6503
- this.server?.close((err) => {
6524
+ if (options.handleSignals !== false) this.installSignalHandlers();
6525
+ }
6526
+ shutdown() {
6527
+ if (this.shutdownPromise) return this.shutdownPromise;
6528
+ this.shuttingDown = true;
6529
+ this.shutdownPromise = this.performShutdown();
6530
+ return this.shutdownPromise;
6531
+ }
6532
+ async performShutdown() {
6533
+ const listenerClosed = new Promise((resolve3, reject) => {
6534
+ if (!this.server) return resolve3();
6535
+ this.server.close((err) => {
6504
6536
  if (err) reject(err);
6505
6537
  else resolve3();
6506
6538
  });
6507
6539
  });
6540
+ try {
6541
+ await this.agentFieldClient.shutdown(this.config.nodeId);
6542
+ } catch (err) {
6543
+ console.warn("[Agent] Failed to notify control plane of shutdown:", err);
6544
+ }
6545
+ if (this.heartbeatTimer) {
6546
+ clearInterval(this.heartbeatTimer);
6547
+ }
6548
+ await listenerClosed;
6549
+ const timeoutMs = parseShutdownTimeout(process.env.AGENTFIELD_SHUTDOWN_TIMEOUT);
6550
+ let timer;
6551
+ const timeout = new Promise((resolve3) => {
6552
+ timer = setTimeout(() => resolve3("timeout"), timeoutMs);
6553
+ });
6554
+ const drained = Promise.allSettled([...this.inFlightExecutions.values()]).then(() => "drained");
6555
+ if (await Promise.race([drained, timeout]) === "timeout") {
6556
+ for (const executionId of this.inFlightExecutions.keys()) {
6557
+ this.cancelRegistry.cancel(executionId, "shutdown_timeout");
6558
+ }
6559
+ this.pauseManager.cancelAll();
6560
+ let settlementTimer;
6561
+ const settlementTimeout = new Promise((resolve3) => {
6562
+ settlementTimer = setTimeout(resolve3, POST_CANCEL_SETTLEMENT_MS);
6563
+ });
6564
+ await Promise.race([
6565
+ Promise.allSettled([...this.inFlightExecutions.values()]),
6566
+ settlementTimeout
6567
+ ]);
6568
+ if (settlementTimer) clearTimeout(settlementTimer);
6569
+ }
6570
+ if (timer) clearTimeout(timer);
6508
6571
  this.memoryEventClient.stop();
6572
+ if (this.signalHandlers) {
6573
+ process.off("SIGTERM", this.signalHandlers.SIGTERM);
6574
+ process.off("SIGINT", this.signalHandlers.SIGINT);
6575
+ this.signalHandlers = void 0;
6576
+ }
6577
+ }
6578
+ installSignalHandlers() {
6579
+ if (this.signalHandlers) return;
6580
+ const handle = (signal) => () => {
6581
+ void this.shutdown().finally(() => {
6582
+ process.exit(signal === "SIGTERM" ? 143 : 130);
6583
+ });
6584
+ };
6585
+ this.signalHandlers = { SIGTERM: handle("SIGTERM"), SIGINT: handle("SIGINT") };
6586
+ process.on("SIGTERM", this.signalHandlers.SIGTERM);
6587
+ process.on("SIGINT", this.signalHandlers.SIGINT);
6509
6588
  }
6510
6589
  async call(target, input) {
6511
6590
  const { agentId, name } = this.parseTarget(target);
@@ -6927,11 +7006,17 @@ var Agent = class {
6927
7006
  this.app.post("/execute/:name", (req, res) => this.executeServerlessHttp(req, res, req.params.name));
6928
7007
  }
6929
7008
  async executeReasoner(req, res, name) {
7009
+ if (this.shuttingDown) {
7010
+ res.status(503).json({ error: "agent shutting down" });
7011
+ return;
7012
+ }
6930
7013
  const metadata = this.buildMetadata(req);
6931
7014
  const reasoner = this.reasoners.get(name);
6932
7015
  if (reasoner && this.shouldRunAsync(req)) {
6933
7016
  res.status(202).json({ status: "processing", execution_id: metadata.executionId });
6934
- void this.runReasonerAsync(reasoner, { targetName: name, input: req.body, metadata });
7017
+ const execution = this.runReasonerAsync(reasoner, { targetName: name, input: req.body, metadata });
7018
+ this.inFlightExecutions.set(metadata.executionId, execution);
7019
+ void execution.finally(() => this.inFlightExecutions.delete(metadata.executionId));
6935
7020
  return;
6936
7021
  }
6937
7022
  try {
@@ -6981,6 +7066,10 @@ var Agent = class {
6981
7066
  return this.buildMetadataFromHeaders(req.headers);
6982
7067
  }
6983
7068
  async executeServerlessHttp(req, res, explicitName) {
7069
+ if (this.shuttingDown) {
7070
+ res.status(503).json({ error: "agent shutting down" });
7071
+ return;
7072
+ }
6984
7073
  const invocation = this.extractInvocationDetails({
6985
7074
  path: req.path,
6986
7075
  explicitTarget: explicitName,