Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
121 changes: 63 additions & 58 deletions ts/docs/architecture/lifecycle/agent-lifecycle.md

Large diffs are not rendered by default.

3 changes: 2 additions & 1 deletion ts/docs/guides/embedding-dispatcher.md
Original file line number Diff line number Diff line change
Expand Up @@ -150,8 +150,9 @@ whatever table you return:
import { createDispatcher, type AppAgentSource } from "agent-dispatcher";

const mySource: AppAgentSource = {
connect(host) {
connect(controller) {
// return { providers: Promise.resolve([...]), dispose } — see AppAgentConnection
// Later changes use controller.runExclusive(mutation => ...).
},
};

Expand Down
5 changes: 4 additions & 1 deletion ts/packages/defaultAgentProvider/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,10 @@ move to the newest matching version (optionally constrained by a `<range>` such
as `^1.4`, `~2.0`, or `>=3 <4`), path agents refresh from their recorded path,
and catalog agents are looked up again by short name. If the recorded source is
no longer configured, the installed agent can still load at runtime, but update
fails until the source is added back or the agent is uninstalled.
fails until the source is added back or the agent is uninstalled. A feed update
reports the old and new package versions when its cross-session swap starts; if
the resolved version is already serving, it reports that immediately and skips
the swap.

### `@package uninstall <name>`

Expand Down
1 change: 1 addition & 0 deletions ts/packages/defaultAgentProvider/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@
"@typeagent/aiclient": "workspace:*",
"@typeagent/common-utils": "workspace:*",
"@typeagent/config": "workspace:*",
"@typeagent/dispatcher-types": "workspace:*",
"agent-cache": "workspace:*",
"agent-dispatcher": "workspace:*",
"browser-typeagent": "workspace:*",
Expand Down
534 changes: 288 additions & 246 deletions ts/packages/defaultAgentProvider/src/defaultAgentProviders.ts

Large diffs are not rendered by default.

41 changes: 31 additions & 10 deletions ts/packages/defaultAgentProvider/src/installSources/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
// `InstallSourceInfo`, and the install-record shapes (`ResolvedCandidate` /
// `InstalledAgentRecord`). These live in the host (default-agent-provider), not
// the dispatcher core: the core knows nothing about how sources are configured,
// listed, resolved, or recorded - it only exposes the per-session `AppAgentHost`
// listed, resolved, or recorded - it only exposes the per-session controller
// the host uses to register and tear down agents. The host contributes the whole
// `@package` command table (`@package install`/`uninstall`/`update`/`list`, with
// the source table nested as `@package source`) via
Expand Down Expand Up @@ -148,6 +148,21 @@ export interface InstallResult {
warnings?: string[];
}

/**
* The immediate disposition of an `@package update` request. `unchanged`
* means the resolved package version is already serving. `started` means the
* cross-session swap was armed and will settle asynchronously; package version
* details are present when the source can determine them.
*/
export type UpdateResult =
| { status: "unchanged" }
| {
status: "started";
packageName?: string;
oldVersion?: string;
newVersion?: string;
};

/**
* The subdirectory under `installDir` that holds the per-agent, version-scoped
* install roots. Each feed install materializes into its own
Expand Down Expand Up @@ -214,7 +229,13 @@ export interface ResolveResult {
* concrete version).
*/
export type InstallSourceUpdateResult =
| { status: "updated"; record: MaterializedInstallRecord }
| {
status: "updated";
record: MaterializedInstallRecord;
packageName?: string;
oldVersion?: string;
newVersion?: string;
}
| { status: "no-op"; record: MaterializedInstallRecord };

/**
Expand All @@ -238,16 +259,16 @@ export type SourceStatus = (message: string) => void;

/**
* The terminal outcome the issuing conversation is told about after a coordinated
* `@package update` settles asynchronously. `updated` = the swap
* `@package update` swap settles asynchronously. `updated` = the swap
* committed to `v2`; `reverted` = a phase timeout (a straggler that would not
* idle, or a `v2` that would not start) rolled back to `v1`, leaving `v1` serving
* in every session; `unchanged` = the requested version was already the installed,
* serving version, so nothing was swapped (a no-op the fan-out cannot express).
* A committed `updated` is announced by the source's cross-session fan-out (like
* an install's add); `reverted` and `unchanged` change no live session state, so
* the fan-out is silent and only the issuing conversation is told.
* idle, or a `v2` that would not start) rolled back to `v1`, leaving `v1`
* serving in every session. An unchanged request is returned immediately as an
* {@link UpdateResult} and never enters the barrier. A committed `updated` is
* announced by the source's cross-session fan-out (like an install's add);
* `reverted` changes no live session state, so only the issuing conversation is
* told.
*/
export type UpdateOutcomeStatus = "updated" | "reverted" | "unchanged";
export type UpdateOutcomeStatus = "updated" | "reverted";

/**
* The terminal outcome the issuing conversation is told about after a coordinated
Expand Down
21 changes: 21 additions & 0 deletions ts/packages/defaultAgentProvider/src/installSources/feedSource.ts
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,18 @@ function installRootFor(moduleName: string, version: string): string {
return `${sanitizeLabel(moduleName)}@${version}`;
}

function versionFromInstallRoot(
moduleName: string,
installRoot: string | undefined,
): string | undefined {
const prefix = `${sanitizeLabel(moduleName)}@`;
if (installRoot === undefined || !installRoot.startsWith(prefix)) {
return undefined;
}
const version = installRoot.slice(prefix.length);
return isConcreteVersion(version) ? version : undefined;
}

// A short, unique, filesystem-safe install-id. Used to
// name the TEMPORARY install root (`.tmp-<id>`) a slow-path materialize installs
// into before atomically adopting it as the content-addressed `module@version`
Expand Down Expand Up @@ -873,9 +885,18 @@ export function createFeedSource(
record: noOpRecord,
};
}
const oldVersion = versionFromInstallRoot(
moduleName,
record.installRoot,
);
return {
status: "updated" as const,
record: await this.materialize(candidate),
packageName: moduleName,
...(oldVersion !== undefined ? { oldVersion } : {}),
...(candidate.version !== undefined
? { newVersion: candidate.version }
: {}),
};
},
async materialize(
Expand Down
Loading
Loading