Repository navigation
Fail Worker creation when GoogleAdkPlugin would replace another Activity (breaking) #161
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,67 @@ | ||
| /** | ||
| * Lets the plugin refuse an Activity name that is already taken. The SDK's | ||
| * plugin merge (`{...existing, ...param}`) keeps the last Activity of each | ||
| * name, so without this check a plugin Activity silently replaces a different | ||
| * Activity with the same name. @internal | ||
| */ | ||
|
|
||
| /** | ||
| * What each plugin Activity is: the name it was built under (its operation) | ||
| * followed by what it depends on. Two Activities with the same operation and | ||
| * the same dependencies do the same thing, so one may replace the other. | ||
| */ | ||
| const activitySources = new WeakMap<object, readonly unknown[]>(); | ||
|
|
||
| /** | ||
| * Records, for every Activity in `activities`, its own key and `sources`, and | ||
| * returns `activities`. The key keeps one operation, registered under another | ||
| * operation's name, from passing as that operation. | ||
| */ | ||
| export function withSources<T extends object>(activities: T, sources: readonly unknown[]): T { | ||
| for (const [operation, fn] of Object.entries(activities)) activitySources.set(fn, [operation, ...sources]); | ||
| return activities; | ||
| } | ||
|
|
||
| /** | ||
| * Throws if an Activity in `added` would replace a different Activity with the | ||
| * same name in `existing`. The same function, or one built for the same | ||
| * operation from the same sources with the same Activity metadata, may replace | ||
| * itself: that is the same plugin instance, or one with the same options, | ||
| * registered twice (e.g. on the NativeConnection and the Worker). | ||
| */ | ||
| export function refuseReplacedActivities(plugin: string, existing: object | undefined, added: object): void { | ||
| // What the Worker registers: own enumerable keys with function values. | ||
| const registered = new Map(Object.entries(existing ?? {}).filter(([, fn]) => typeof fn === 'function')); | ||
| for (const [name, fn] of Object.entries(added)) { | ||
| const other = registered.get(name); | ||
| if (other === undefined || other === fn || sameSources(other, fn)) continue; | ||
| throw new TypeError( | ||
| `Duplicate registration of Activity '${name}': ${plugin} would replace a different Activity with this name. ` + | ||
| `Rename or remove the other registration, or give each ${plugin} on this Worker the same options.` | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. P3: the error tells the user to 'give each GoogleAdkPlugin on this Worker the same options', which reads as value equality and does not name the fix for the two likeliest two-instance layouts. Probed on the head with real
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. #161 merged before this was addressed, so the fix is in #339. The message there is the same as #162 and #202: |
||
| ); | ||
| } | ||
| } | ||
|
|
||
| function sameSources(a: object, b: object): boolean { | ||
| const x = activitySources.get(a); | ||
| const y = activitySources.get(b); | ||
| return ( | ||
| x !== undefined && | ||
| y !== undefined && | ||
| x.length === y.length && | ||
| x.every((v, i) => v === y[i]) && | ||
| activityOptions(a) === activityOptions(b) | ||
| ); | ||
| } | ||
|
|
||
| /** | ||
| * The metadata `setActivityOptions` attached to `fn`, read the way the Worker | ||
| * reads it (an own object-valued `activityDefinitionOptions`). Its `typeInfo` | ||
| * decodes the Activity's arguments and encodes its result, so a function with | ||
| * other metadata is a different Activity even when its sources match. | ||
| */ | ||
| function activityOptions(fn: object): object | undefined { | ||
| if (!Object.hasOwn(fn, 'activityDefinitionOptions')) return undefined; | ||
| const options: unknown = (fn as { activityDefinitionOptions?: unknown }).activityDefinitionOptions; | ||
| return typeof options === 'object' && options !== null ? options : undefined; | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
P3: the list of refused setups is narrower than the rule, and 'Earlier versions kept the last one' is still true for a plugin listed after this one. (a) The rule includes the provider's options object (
activities.ts:153; the JSDoc atplugin.ts:619-620says so), so the samemodelProviderin two options objects is refused, and so is a plugin without amodelProvidernext to one with it ([new GoogleAdkPlugin({ mcpToolsets }), new GoogleAdkPlugin({ modelProvider })], which on main ended up with the provider plugin's model Activities). Both read as supported here, because neither has 'a differentmodelProvideror toolset factory'. (b) The check only sees Activities registered before this plugin'sconfigureWorkerruns:plugins: [new GoogleAdkPlugin(), new SimplePlugin({ name: 'mine', activities: { 'adk-invokeModel': mine } })]starts and registersmine(probe), andAiSdkPluginafter this plugin does the same until #162 lands. Suggested wording: 'TwoGoogleAdkPlugins on one Worker must be the same instance or be built from one options object; two without amodelProvideralso work when every shared toolset name uses the same factory function.' and 'A plugin listed afterGoogleAdkPlugincan still replace these Activities unless it runs the same check.' Also, the 'unlesssetActivityOptions...' clause cannot apply to the same instance (its functions are identical), so it belongs to the second case only.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Addressed in #339, since #161 merged first. (a) The README now lists the setups the rule refuses, including one
modelProviderin two options objects and a plugin without amodelProvidernext to one with it. (b)runWorkernow refuses a plugin applied afterGoogleAdkPlugin, so a later replacement failsworker.run()before the Worker polls. The README says which registrations fail atWorker.createand which atworker.run().