@@ -13,6 +13,7 @@ import (
1313 "github.com/kagent-dev/kagent/go/adk/pkg/session"
1414 "github.com/kagent-dev/kagent/go/adk/pkg/skills"
1515 "github.com/kagent-dev/kagent/go/adk/pkg/telemetry"
16+ "go.opentelemetry.io/otel/attribute"
1617 adkagent "google.golang.org/adk/agent"
1718 "google.golang.org/adk/runner"
1819 "google.golang.org/adk/server/adka2a"
@@ -130,6 +131,8 @@ func (e *KAgentExecutor) Execute(ctx context.Context, reqCtx *a2asrv.RequestCont
130131 spanAttributes ["kagent.app_name" ] = e .appName
131132 }
132133 ctx = telemetry .SetKAgentSpanAttributes (ctx , spanAttributes )
134+ ctx , invocationSpan := telemetry .StartInvocationSpan (ctx )
135+ defer invocationSpan .End ()
133136
134137 // 3. Initialize skills session path.
135138 if e .skillsDirectory != "" && sessionID != "" {
@@ -189,11 +192,20 @@ func (e *KAgentExecutor) Execute(ctx context.Context, reqCtx *a2asrv.RequestCont
189192
190193 // 9. Emit initial events.
191194 if reqCtx .StoredTask == nil {
192- // New task — emit submitted.
193- submitted := a2atype .NewStatusUpdateEvent (reqCtx , a2atype .TaskStateSubmitted , nil )
195+ // New task — emit submitted with the user's message
196+ submitted := a2atype .NewStatusUpdateEvent (reqCtx , a2atype .TaskStateSubmitted , reqCtx . Message )
194197 if err := queue .Write (ctx , submitted ); err != nil {
195198 return fmt .Errorf ("failed to write submitted event: %w" , err )
196199 }
200+ } else if ExtractDecisionFromMessage (reqCtx .Message ) != "" {
201+ // a2a-go appends incoming message to task history before executor runs.
202+ // See https://github.com/a2aproject/a2a-go/blob/v0.3.13/a2asrv/agentexec.go#L188
203+ // Remove the pre-appended copy and emit one decision status event.
204+ dropPreAppendedDecisionFromHistory (reqCtx .StoredTask , reqCtx .Message )
205+ decision := a2atype .NewStatusUpdateEvent (reqCtx , a2atype .TaskStateWorking , reqCtx .Message )
206+ if err := queue .Write (ctx , decision ); err != nil {
207+ return fmt .Errorf ("failed to write HITL decision status event: %w" , err )
208+ }
197209 }
198210
199211 // Base metadata carried on every event (app_name, user_id, session_id).
@@ -217,13 +229,10 @@ func (e *KAgentExecutor) Execute(ctx context.Context, reqCtx *a2asrv.RequestCont
217229
218230 // State tracked across the event loop.
219231 var (
220- invocationID string
221- artifactID a2atype.ArtifactID
222- partialArtifactID a2atype.ArtifactID
223- lastTextParts a2atype.ContentParts
224- lastTextMeta map [string ]any
225- hitlParts a2atype.ContentParts
226- runErr error
232+ invocationID string
233+ lastNonPartialParts a2atype.ContentParts
234+ hitlParts a2atype.ContentParts
235+ runErr error
227236 )
228237
229238 for adkEvent , adkErr := range r .Run (ctx , userID , sessionID , content , runConfig ) {
@@ -238,6 +247,7 @@ func (e *KAgentExecutor) Execute(ctx context.Context, reqCtx *a2asrv.RequestCont
238247 // Track invocation ID from the first event that has one.
239248 if adkEvent .InvocationID != "" && invocationID == "" {
240249 invocationID = adkEvent .InvocationID
250+ invocationSpan .SetAttributes (attribute .String ("gcp.vertex.agent.invocation_id" , invocationID ))
241251 }
242252
243253 // Build per-event metadata (inherits baseMeta + adds invocation_id, usage etc.).
@@ -299,8 +309,11 @@ func (e *KAgentExecutor) Execute(ctx context.Context, reqCtx *a2asrv.RequestCont
299309 }
300310
301311 if adkEvent .Partial {
302- // Partial event: emit as working status (text-only) for UI streaming,
303- // and as partial artifact update.
312+ // Partial event: emit as working status (text-only) for UI streaming.
313+ // Note: Go ADK executor uses TaskArtifactUpdateEvent for partial events,
314+ // so we don't need to emit a separate partial artifact update.
315+ // However, this is done here in order to match the Python executor's behavior.
316+ // Go ADK executor also uses different A2A response formats than Python ADK.
304317 textOnly := filterTextParts (a2aParts )
305318 if len (textOnly ) > 0 {
306319 mirrorMeta := maps .Clone (eventMeta )
@@ -313,28 +326,7 @@ func (e *KAgentExecutor) Execute(ctx context.Context, reqCtx *a2asrv.RequestCont
313326 return fmt .Errorf ("failed to write partial status event: %w" , err )
314327 }
315328 }
316-
317- // Emit partial artifact update.
318- var partialEv * a2atype.TaskArtifactUpdateEvent
319- if partialArtifactID == "" {
320- partialEv = a2atype .NewArtifactEvent (reqCtx , a2aParts ... )
321- partialArtifactID = partialEv .Artifact .ID
322- } else {
323- partialEv = a2atype .NewArtifactUpdateEvent (reqCtx , partialArtifactID , a2aParts ... )
324- }
325- if partialEv .Artifact .Metadata == nil {
326- partialEv .Artifact .Metadata = map [string ]any {}
327- }
328- partialEv .Artifact .Metadata [adka2a .ToA2AMetaKey ("partial" )] = true
329- partialEv .Metadata = maps .Clone (eventMeta )
330- partialEv .Metadata [adka2a .ToA2AMetaKey ("partial" )] = true
331- partialEv .Append = false // discard partial events
332- if err := queue .Write (ctx , partialEv ); err != nil {
333- return fmt .Errorf ("failed to write partial artifact event: %w" , err )
334- }
335329 } else {
336- // Non-partial event: emit as artifact + mirror as working status.
337- // Mirror: emit working status with non-empty parts.
338330 mirrorParts := a2aParts
339331 if len (hitlParts ) == 0 {
340332 // Only mirror when not accumulating HITL parts (those go into input_required).
@@ -345,27 +337,7 @@ func (e *KAgentExecutor) Execute(ctx context.Context, reqCtx *a2asrv.RequestCont
345337 if err := queue .Write (ctx , statusEv ); err != nil {
346338 return fmt .Errorf ("failed to write mirror status event: %w" , err )
347339 }
348- }
349-
350- // Track last text parts for final status injection.
351- if tp := filterTextParts (a2aParts ); len (tp ) > 0 {
352- lastTextParts = tp
353- lastTextMeta = eventMeta
354- }
355-
356- // Emit artifact.
357- var artifactEv * a2atype.TaskArtifactUpdateEvent
358- if artifactID == "" {
359- artifactEv = a2atype .NewArtifactEvent (reqCtx , a2aParts ... )
360- artifactID = artifactEv .Artifact .ID
361- } else {
362- artifactEv = a2atype .NewArtifactUpdateEvent (reqCtx , artifactID , a2aParts ... )
363- }
364- // Mark as non-partial so downstream consumers skip their own aggregation.
365- artifactEv .Metadata = maps .Clone (eventMeta )
366- artifactEv .Metadata [adka2a .ToA2AMetaKey ("partial" )] = false
367- if err := queue .Write (ctx , artifactEv ); err != nil {
368- return fmt .Errorf ("failed to write artifact event: %w" , err )
340+ lastNonPartialParts = mirrorParts
369341 }
370342 }
371343
@@ -398,15 +370,16 @@ func (e *KAgentExecutor) Execute(ctx context.Context, reqCtx *a2asrv.RequestCont
398370 return queue .Write (ctx , inputRequired )
399371 }
400372
401- // completed: inject last text into final status if no message present .
402- var finalMsg * a2atype. Message
403- if len ( lastTextParts ) > 0 {
404- finalMsg = a2atype . NewMessage ( a2atype . MessageRoleAgent , lastTextParts ... )
405- if len ( lastTextMeta ) > 0 {
406- finalMsg . Metadata = maps . Clone ( lastTextMeta )
373+ // Final artifact update with lastChunk=true (if we have parts) and final completed status update ( no message payload) .
374+ if len ( lastNonPartialParts ) > 0 {
375+ finalArtifact := a2atype . NewArtifactEvent ( reqCtx , lastNonPartialParts ... )
376+ finalArtifact . LastChunk = true
377+ if err := queue . Write ( ctx , finalArtifact ); err != nil {
378+ return fmt . Errorf ( "failed to write final artifact event: %w" , err )
407379 }
408380 }
409- completed := a2atype .NewStatusUpdateEvent (reqCtx , a2atype .TaskStateCompleted , finalMsg )
381+
382+ completed := a2atype .NewStatusUpdateEvent (reqCtx , a2atype .TaskStateCompleted , nil )
410383 completed .Final = true
411384 completed .Metadata = finalMeta
412385 return queue .Write (ctx , completed )
@@ -434,3 +407,19 @@ func extractSessionName(message *a2atype.Message) string {
434407 }
435408 return ""
436409}
410+
411+ // dropPreAppendedDecisionFromHistory removes a pre-appended HITL decision
412+ // message inserted by a2a-go before executor invocation.
413+ func dropPreAppendedDecisionFromHistory (task * a2atype.Task , incoming * a2atype.Message ) {
414+ if task == nil || incoming == nil || len (task .History ) == 0 {
415+ return
416+ }
417+ last := task .History [len (task .History )- 1 ]
418+ if last == nil || last .ID != incoming .ID {
419+ return
420+ }
421+ if ExtractDecisionFromMessage (last ) == "" {
422+ return
423+ }
424+ task .History = task .History [:len (task .History )- 1 ]
425+ }
0 commit comments